From 9666345699b95439d938ea6f17f92f7f8921dd78 Mon Sep 17 00:00:00 2001
From: blavenie <benoit.lavenier@e-is.pro>
Date: Fri, 24 Mar 2017 13:28:48 +0100
Subject: [PATCH] - Start peer indexation from Duniter network - new mavn
 submodule, for command line tool

---
 duniter4j-cmd/lib/j-text-utils-0.3.3.jar      | Bin 0 -> 18856 bytes
 duniter4j-cmd/lib/j-text-utils-0.3.3.pom      |  65 ++
 duniter4j-cmd/pom.xml                         | 156 +++++
 .../src/main/java/fr/duniter/cmd/Main.java    | 149 +++++
 .../fr/duniter/cmd/actions/NetworkAction.java |  80 +++
 .../duniter/cmd/actions/SentMoneyAction.java  |  76 +++
 .../cmd/actions/params/WalletParameters.java  |  14 +
 .../duniter/cmd/actions/utils/Formatters.java |  34 ++
 .../services/org.duniter.core.beans.Bean      |  13 +
 .../src/main/resources/duniter4j-cmd.config   |   5 +
 .../src/main/resources/log4j.properties       |  27 +
 .../core/client/config/Configuration.java     |  12 +-
 .../client/config/ConfigurationOption.java    |   6 +-
 .../core/client/model/bma/Constants.java      |   9 +
 ...EndpointProtocol.java => EndpointApi.java} |   5 +-
 .../core/client/model/bma/NetworkPeering.java |  25 +-
 .../core/client/model/bma/NetworkPeers.java   |   4 +-
 .../core/client/model/bma/Protocol.java       |   4 +-
 .../core/client/model/bma/TxSource.java       |   2 +-
 .../model/bma/gson/EndpointAdapter.java       |  18 +-
 .../model/bma/gson/MultimapTypeAdapter.java   |   2 +-
 .../bma/jackson/EndpointDeserializer.java     | 121 +++-
 .../model/elasticsearch/DeleteRecord.java     |   2 +-
 .../duniter/core/client/model/local/Peer.java | 445 ++++++++++++--
 .../core/client/service/HttpServiceImpl.java  |  33 +-
 .../core/client/service/ServiceLocator.java   |  12 +-
 .../service/bma/BlockchainRemoteService.java  |  18 +-
 .../bma/BlockchainRemoteServiceImpl.java      |  48 +-
 .../service/bma/NetworkRemoteService.java     |  12 +-
 .../service/bma/NetworkRemoteServiceImpl.java | 148 ++++-
 .../bma/TransactionRemoteServiceImpl.java     |   4 +-
 .../client/service/bma/WotRemoteService.java  |   5 +
 .../service/bma/WotRemoteServiceImpl.java     |  32 +
 .../CurrencyRegistryRemoteServiceImpl.java    |   4 +-
 .../client/service/local/CurrencyService.java |   2 +-
 .../service/local/CurrencyServiceImpl.java    |   2 +-
 .../client/service/local/NetworkService.java  |  57 ++
 .../service/local/NetworkServiceImpl.java     | 444 ++++++++++++++
 .../client/service/local/PeerServiceImpl.java |   2 +-
 .../org/duniter/core/client/TestResource.java |   5 +-
 .../core/client/service/HttpServiceTest.java  |  15 +-
 .../bma/BlockchainRemoteServiceTest.java      |  11 +-
 .../service/bma/NetworkRemoteServiceTest.java |  13 +-
 .../bma/TransactionRemoteServiceTest.java     |   9 +-
 .../service/bma/WotRemoteServiceTest.java     |   9 +-
 .../service/local/NetworkServiceTest.java     |  72 +++
 .../services/org.duniter.core.beans.Bean      |   1 +
 .../duniter4j-core-client-test.properties     |   4 +-
 .../src/test/resources/log4j.properties       |  18 +-
 duniter4j-core-shared/pom.xml                 |  10 +
 .../org/duniter/core/beans/BeanFactory.java   |   1 -
 .../util/concurrent/CompletableFutures.java   |  38 ++
 .../core/util/http/InetAddressUtils.java      |  27 +
 .../websocket/WebsocketClientEndpoint.java    |  23 +
 .../org/duniter/elasticsearch/PluginInit.java |   6 +
 .../duniter/elasticsearch/PluginSettings.java |   5 +-
 .../service/BlockchainService.java            |  40 +-
 .../elasticsearch/service/NetworkService.java | 571 ++++++++++++++++++
 .../elasticsearch/service/ServiceLocator.java |   7 +-
 .../elasticsearch/service/ServiceModule.java  |   3 +-
 .../elasticsearch/threadpool/ThreadPool.java  |   5 +-
 .../services/org.duniter.core.beans.Bean      |   1 +
 .../i18n/duniter4j-es-core_en_GB.properties   |   5 +
 .../i18n/duniter4j-es-core_fr_FR.properties   |   5 +
 .../service/CitiesRegistryService.java        |   2 +-
 pom.xml                                       |   3 +-
 66 files changed, 2756 insertions(+), 250 deletions(-)
 create mode 100644 duniter4j-cmd/lib/j-text-utils-0.3.3.jar
 create mode 100644 duniter4j-cmd/lib/j-text-utils-0.3.3.pom
 create mode 100644 duniter4j-cmd/pom.xml
 create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/Main.java
 create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/NetworkAction.java
 create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/SentMoneyAction.java
 create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/params/WalletParameters.java
 create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/utils/Formatters.java
 create mode 100644 duniter4j-cmd/src/main/resources/META-INF/services/org.duniter.core.beans.Bean
 create mode 100644 duniter4j-cmd/src/main/resources/duniter4j-cmd.config
 create mode 100644 duniter4j-cmd/src/main/resources/log4j.properties
 rename duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/{EndpointProtocol.java => EndpointApi.java} (92%)
 create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java
 create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java
 create mode 100644 duniter4j-core-client/src/test/java/org/duniter/core/client/service/local/NetworkServiceTest.java
 create mode 100644 duniter4j-core-shared/src/main/java/org/duniter/core/util/concurrent/CompletableFutures.java
 create mode 100644 duniter4j-core-shared/src/main/java/org/duniter/core/util/http/InetAddressUtils.java
 create mode 100644 duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/NetworkService.java

diff --git a/duniter4j-cmd/lib/j-text-utils-0.3.3.jar b/duniter4j-cmd/lib/j-text-utils-0.3.3.jar
new file mode 100644
index 0000000000000000000000000000000000000000..42a9c6fa4ce334d113eba5af341d8a06b1a651e7
GIT binary patch
literal 18856
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
V2@dfC1^4v_?v?!n^8NVs{{Wxw$6EjZ

literal 0
HcmV?d00001

diff --git a/duniter4j-cmd/lib/j-text-utils-0.3.3.pom b/duniter4j-cmd/lib/j-text-utils-0.3.3.pom
new file mode 100644
index 00000000..1a68c2db
--- /dev/null
+++ b/duniter4j-cmd/lib/j-text-utils-0.3.3.pom
@@ -0,0 +1,65 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>dnl.utils</groupId>
+    <artifactId>j-text-utils</artifactId>
+    <packaging>jar</packaging>
+    <version>0.3.3</version>
+    <name>Java Text Utilities</name>
+    <url>http://code.google.com/p/j-text-utils</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.4</version>
+        </dependency>
+        <dependency>
+            <groupId>net.sf.opencsv</groupId>
+            <artifactId>opencsv</artifactId>
+            <version>2.3</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>14.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.7</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <extensions>
+            <extension>
+                <groupId>org.jvnet.wagon-svn</groupId>
+                <artifactId>wagon-svn</artifactId>
+                <version>1.9</version>
+            </extension>
+        </extensions>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <distributionManagement>
+        <repository>
+            <uniqueVersion>false</uniqueVersion>
+            <id>googlecode</id>
+            <url>svn:https://j-text-utils.googlecode.com/svn/trunk/repo/</url>
+        </repository>
+    </distributionManagement>
+
+</project>
diff --git a/duniter4j-cmd/pom.xml b/duniter4j-cmd/pom.xml
new file mode 100644
index 00000000..0491353b
--- /dev/null
+++ b/duniter4j-cmd/pom.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>duniter4j</artifactId>
+        <groupId>org.duniter</groupId>
+        <version>0.9.2-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>duniter4j-cmd</artifactId>
+
+    <properties>
+        <jTextUtilsVersion>0.3.3</jTextUtilsVersion>
+    </properties>
+
+    <repositories>
+        <repository>
+            <id>d-maven</id>
+            <url>https://github.com/neilpanchal/j-text-utils/tree/master/repo</url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.duniter</groupId>
+            <artifactId>duniter4j-core-client</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <!-- logging -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+        </dependency>
+
+
+        <dependency>
+            <groupId>com.beust</groupId>
+            <artifactId>jcommander</artifactId>
+            <version>1.60</version>
+        </dependency>
+
+        <dependency>
+            <groupId>dnl.utils</groupId>
+            <artifactId>j-text-utils</artifactId>
+            <version>${jTextUtilsVersion}</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.6</version>
+        </dependency>
+
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>install-missing-libs</id>
+            <activation>
+                <file>
+                    <missing>${settings.localRepository}/dnl/utils/j-text-utils/${jTextUtilsVersion}/j-text-utils-${jTextUtilsVersion}.jar</missing>
+                </file>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-install-plugin</artifactId>
+                        <version>2.5.2</version>
+                        <executions>
+                            <execution>
+                                <id>installing j-text-utils.jar</id>
+                                <phase>initialize</phase>
+                                <goals>
+                                    <goal>install-file</goal>
+                                </goals>
+                                <configuration>
+                                    <groupId>dnl.utils</groupId>
+                                    <artifactId>j-text-utils</artifactId>
+                                    <version>${jTextUtilsVersion}</version>
+                                    <packaging>jar</packaging>
+                                    <file>${project.basedir}/lib/j-text-utils-${jTextUtilsVersion}.jar</file>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                    <plugin>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>enforce-dependencies-exists</id>
+                                <phase>generate-sources</phase>
+                                <goals>
+                                    <goal>run</goal>
+                                </goals>
+                                <configuration>
+                                    <target>
+
+                                        <condition property="displayMessage">
+                                            <and>
+                                                <not><available file="${project.basedir}/.maven/install.log" /></not>
+                                                <!-- do not failed here if performRelease -->
+                                                <isfalse value="${performRelease}" />
+                                            </and>
+                                        </condition>
+                                        <property name="installSuccessMessage">*
+                                            *************************************************************************
+                                            *
+                                            * IMPORTANT:
+                                            *
+                                            * Missing lib dependencies successfully installed on [${settings.localRepository}]
+                                            * You should now re-run the build.
+                                            * This message will NOT appear again
+                                            *
+                                            *************************************************************************
+                                        </property>
+
+                                        <echo file="${project.basedir}/.maven/install.log">${installSuccessMessage}</echo>
+
+                                        <fail message="${installSuccessMessage}" >
+                                            <condition>
+                                                <istrue value="${displayMessage}"/>
+                                            </condition>
+                                        </fail>
+                                    </target>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+
+        <profile>
+            <id>use-installed-libs</id>
+            <activation>
+                <file>
+                    <exists>${settings.localRepository}/dnl/utils/j-text-utils/${jTextUtilsVersion}/j-text-utils-${jTextUtilsVersion}.jar</exists>
+                </file>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>dnl.utils</groupId>
+                    <artifactId>j-text-utils</artifactId>
+                    <version>${jTextUtilsVersion}</version>
+                </dependency>
+            </dependencies>
+        </profile>
+    </profiles>
+</project>
\ No newline at end of file
diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/Main.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/Main.java
new file mode 100644
index 00000000..7208a3dc
--- /dev/null
+++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/Main.java
@@ -0,0 +1,149 @@
+package fr.duniter.cmd;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.google.common.collect.Lists;
+import fr.duniter.cmd.actions.NetworkAction;
+import fr.duniter.cmd.actions.SentMoneyAction;
+import org.apache.commons.io.FileUtils;
+import org.duniter.core.client.config.Configuration;
+import org.duniter.core.client.service.ServiceLocator;
+import org.duniter.core.util.StringUtils;
+import org.nuiton.i18n.I18n;
+import org.nuiton.i18n.init.DefaultI18nInitializer;
+import org.nuiton.i18n.init.UserI18nInitializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Created by blavenie on 22/03/17.
+ */
+public class Main {
+
+    @Parameter(names = "-debug", description = "Debug mode", arity = 1)
+    private boolean debug = false;
+
+    @Parameter(names = "--help", help = true)
+    private boolean help;
+
+    @Parameter(names = "-config", description = "Configuration file path")
+    private String configFilename = "duniter-cmd.config";
+
+    public static void main(String ... args) {
+        Main main = new Main();
+        main.run(args);
+    }
+
+    protected void run(String ... args) {
+
+        Map<String, Runnable> actions = new HashMap<>();
+        actions.put("network", new NetworkAction());
+        actions.put("send", new SentMoneyAction());
+
+        // Parsing args
+        JCommander jc = new JCommander(this);
+        actions.entrySet().stream().forEach(entry -> jc.addCommand(entry.getKey(), entry.getValue()));
+        try {
+            jc.parse(args);
+        }
+        catch(ParameterException e) {
+            System.err.println(e.getMessage());
+            System.err.println("Try --help for usage");
+            //jc.usage();
+            System.exit(-1);
+        }
+
+        // Usage, if help or no command
+        String actionName = jc.getParsedCommand();
+        if (StringUtils.isBlank(actionName)) {
+            jc.usage();
+            // Return error code, if not help
+            if (!help) System.exit(-1);
+            return;
+        }
+
+        // Set log level
+        // TODO
+
+        // Init configuration
+        initConfiguration(configFilename);
+
+        // Init i18n
+        try {
+            initI18n();
+        } catch(IOException e) {
+            System.out.println("Unable to initialize translations");
+            System.exit(-1);
+        }
+
+        // Set a default account id, then load cache
+        ServiceLocator.instance().getDataContext().setAccountId(0);
+
+        // Initialize service locator
+        ServiceLocator.instance().init();
+
+        Runnable action = actions.get(actionName);
+        action.run();
+    }
+
+
+    protected String getI18nBundleName() {
+        return "duniter4j-core-client-i18n";
+    }
+
+    /* -- -- */
+
+    /**
+     * Convenience methods that could be override to initialize other configuration
+     *
+     * @param configFilename
+     * @param configArgs
+     */
+    protected void initConfiguration(String configFilename) {
+        String[] configArgs = getConfigArgs();
+        Configuration config = new Configuration(configFilename, configArgs);
+        Configuration.setInstance(config);
+    }
+
+    protected void initI18n() throws IOException {
+        Configuration config = Configuration.instance();
+
+        // --------------------------------------------------------------------//
+        // init i18n
+        // --------------------------------------------------------------------//
+        File i18nDirectory = new File(config.getDataDirectory(), "i18n");
+        if (i18nDirectory.exists()) {
+            // clean i18n cache
+            FileUtils.cleanDirectory(i18nDirectory);
+        }
+
+        FileUtils.forceMkdir(i18nDirectory);
+
+        if (debug) {
+            System.out.println("I18N directory: " + i18nDirectory);
+        }
+
+        Locale i18nLocale = config.getI18nLocale();
+
+        if (debug) {
+            System.out.println(String.format("Starts i18n with locale [%s] at [%s]",
+                    i18nLocale, i18nDirectory));
+        }
+        I18n.init(new UserI18nInitializer(
+                        i18nDirectory, new DefaultI18nInitializer(getI18nBundleName())),
+                i18nLocale);
+    }
+
+    protected String[] getConfigArgs() {
+        List<String> configArgs = Lists.newArrayList();
+        /*configArgs.addAll(Lists.newArrayList(
+                "--option", ConfigurationOption.BASEDIR.getKey(), getResourceDirectory().getAbsolutePath()));*/
+        return configArgs.toArray(new String[configArgs.size()]);
+    }
+
+}
diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/NetworkAction.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/NetworkAction.java
new file mode 100644
index 00000000..ac10b861
--- /dev/null
+++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/NetworkAction.java
@@ -0,0 +1,80 @@
+package fr.duniter.cmd.actions;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import dnl.utils.text.table.TextTable;
+import fr.duniter.cmd.actions.utils.Formatters;
+import org.duniter.core.client.model.local.Peer;
+import org.duniter.core.client.service.ServiceLocator;
+import org.duniter.core.client.service.local.NetworkService;
+import org.duniter.core.util.CollectionUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Created by blavenie on 22/03/17.
+ */
+@Parameters(commandDescription = "Display network peers")
+public class NetworkAction implements Runnable {
+
+    @Parameter(names = "-host", description = "Duniter host")
+    private String host = "g1.duniter.org";
+
+    @Parameter(names = "-port", description = "Duniter port")
+    private int port = 10901;
+
+    @Override
+    public void run() {
+        NetworkService service = ServiceLocator.instance().getNetworkService();
+        Peer mainPeer = Peer.newBuilder().setHost(host).setPort(port).build();
+
+        List<Peer> peers = service.getPeers(mainPeer);
+
+        if (CollectionUtils.isEmpty(peers)) {
+            System.out.println("No peers found");
+        }
+        else {
+
+            String[] columnNames = {
+                    "Uid",
+                    "Pubkey",
+                    "Address",
+                    "Status",
+                    "API",
+                    "Version",
+                    "Difficulty",
+                    "Block #"};
+
+            List<Object[]> data = peers.stream().map(peer -> {
+                    boolean isUp = peer.getStats().getStatus() == Peer.PeerStatus.UP;
+                    return new Object[] {
+                            Formatters.formatUid(peer.getStats().getUid()),
+                            Formatters.formatPubkey(peer.getPubkey()),
+                            peer.getHost() + ":" + peer.getPort(),
+                            peer.getStats().getStatus().name(),
+                            isUp && peer.isUseSsl() ? "SSL" : null,
+                            isUp ? peer.getStats().getVersion() : null,
+                            isUp ? peer.getStats().getHardshipLevel() : "Mirror",
+                            isUp ? peer.getStats().getBlockNumber() : null
+                    };
+                })
+                    .collect(Collectors.toList());
+
+            Object[][] rows = new Object[data.size()][];
+            int i = 0;
+            for (Object[] row : data) {
+                rows[i++] = row;
+            }
+
+
+            TextTable tt = new TextTable(columnNames, rows);
+            // this adds the numbering on the left
+            tt.setAddRowNumbering(true);
+            tt.printTable();
+        }
+
+    }
+
+
+}
diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/SentMoneyAction.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/SentMoneyAction.java
new file mode 100644
index 00000000..9b64dcf8
--- /dev/null
+++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/SentMoneyAction.java
@@ -0,0 +1,76 @@
+package fr.duniter.cmd.actions;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParametersDelegate;
+import fr.duniter.cmd.actions.params.WalletParameters;
+import fr.duniter.cmd.actions.utils.Formatters;
+import org.duniter.core.client.config.Configuration;
+import org.duniter.core.client.model.bma.BlockchainParameters;
+import org.duniter.core.client.model.local.Currency;
+import org.duniter.core.client.model.local.Peer;
+import org.duniter.core.client.model.local.Wallet;
+import org.duniter.core.client.service.ServiceLocator;
+import org.duniter.core.client.service.bma.BlockchainRemoteService;
+import org.duniter.core.client.service.bma.TransactionRemoteService;
+import org.duniter.core.service.CryptoService;
+import org.duniter.core.util.crypto.CryptoUtils;
+import org.duniter.core.util.crypto.KeyPair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Created by blavenie on 22/03/17.
+ */
+public class SentMoneyAction implements Runnable {
+
+    private static final Logger log = LoggerFactory.getLogger(SentMoneyAction.class);
+
+    @ParametersDelegate
+    private WalletParameters walletParams = new WalletParameters();
+
+    @Parameter(names = "--amount", description = "Amount", required = true)
+    public int amount;
+
+    @Parameter(names = "--dest", description = "Destination pubkey", required = true)
+    public String destPubkey;
+
+    @Parameter(names = "--comment", description = "TX Comment")
+    public String comment;
+
+    @Override
+    public void run() {
+
+        CryptoService cryptoService = ServiceLocator.instance().getCryptoService();
+        TransactionRemoteService txService = ServiceLocator.instance().getTransactionRemoteService();
+        Configuration config = Configuration.instance();
+
+        Peer peer = Peer.newBuilder().setHost(config.getNodeHost())
+                .setPort(config.getNodePort())
+                .build();
+
+        Currency currency = ServiceLocator.instance().getBlockchainRemoteService().getCurrencyFromPeer(peer);
+        ServiceLocator.instance().getCurrencyService().save(currency);
+        peer.setCurrencyId(currency.getId());
+        peer.setCurrency(currency.getCurrencyName());
+        ServiceLocator.instance().getPeerService().save(peer);
+
+        // Compute keypair and wallet
+        KeyPair keypair = cryptoService.getKeyPair(walletParams.salt, walletParams.password);
+        Wallet wallet = new Wallet(
+                currency.getCurrencyName(),
+                null,
+                keypair.getPubKey(),
+                keypair.getSecKey());
+        wallet.setCurrencyId(currency.getId());
+
+        System.out.println("Connected to wallet: " + wallet.getPubKeyHash());
+
+        txService.transfer(wallet, destPubkey, amount, comment);
+
+
+        System.out.println(String.format("Successfully sent [%d %s] to [%s]",
+                amount,
+                Formatters.currencySymbol(currency.getCurrencyName()),
+                Formatters.formatPubkey(destPubkey)));
+    }
+}
diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/params/WalletParameters.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/params/WalletParameters.java
new file mode 100644
index 00000000..2694cafc
--- /dev/null
+++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/params/WalletParameters.java
@@ -0,0 +1,14 @@
+package fr.duniter.cmd.actions.params;
+
+import com.beust.jcommander.Parameter;
+
+/**
+ * Created by blavenie on 22/03/17.
+ */
+public class WalletParameters {
+    @Parameter(names = "--salt", description = "Salt (to generate the keypair)", required = true)
+    public String salt;
+
+    @Parameter(names = "--passwd", description = "Password (to generate the keypair)", required = true)
+    public String password;
+}
diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/utils/Formatters.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/utils/Formatters.java
new file mode 100644
index 00000000..0f8cdc9b
--- /dev/null
+++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/utils/Formatters.java
@@ -0,0 +1,34 @@
+package fr.duniter.cmd.actions.utils;
+
+/**
+ * Created by blavenie on 24/03/17.
+ */
+public class Formatters {
+
+    public static String formatPubkey(String pubkey) {
+        if (pubkey != null && pubkey.length() > 8) {
+            return pubkey.substring(0, 8);
+        }
+        return pubkey;
+    }
+
+    public static String formatUid(String uid) {
+        if (uid != null && uid.length() > 20) {
+            return uid.substring(0, 19);
+        }
+        return uid;
+    }
+
+    public static String currencySymbol(String currencyName) {
+        String[] parts = currencyName.split("-_");
+        if (parts.length < 2) {
+            if (currencyName.length() <= 3) {
+                return currencyName.toUpperCase();
+            }
+            else {
+                return currencyName.toUpperCase().substring(0,1);
+            }
+        }
+        return currencySymbol(parts[0]) + currencySymbol(parts[1]);
+    }
+}
diff --git a/duniter4j-cmd/src/main/resources/META-INF/services/org.duniter.core.beans.Bean b/duniter4j-cmd/src/main/resources/META-INF/services/org.duniter.core.beans.Bean
new file mode 100644
index 00000000..bbf9fb6b
--- /dev/null
+++ b/duniter4j-cmd/src/main/resources/META-INF/services/org.duniter.core.beans.Bean
@@ -0,0 +1,13 @@
+org.duniter.core.client.service.bma.BlockchainRemoteServiceImpl
+org.duniter.core.client.service.bma.NetworkRemoteServiceImpl
+org.duniter.core.client.service.bma.WotRemoteServiceImpl
+org.duniter.core.client.service.bma.TransactionRemoteServiceImpl
+org.duniter.core.client.service.elasticsearch.CurrencyRegistryRemoteServiceImpl
+org.duniter.core.service.Ed25519CryptoServiceImpl
+org.duniter.core.client.service.HttpServiceImpl
+org.duniter.core.client.service.DataContext
+org.duniter.core.client.service.local.PeerServiceImpl
+org.duniter.core.client.service.local.CurrencyServiceImpl
+org.duniter.core.client.service.local.NetworkServiceImpl
+org.duniter.core.client.dao.mem.MemoryCurrencyDaoImpl
+org.duniter.core.client.dao.mem.MemoryPeerDaoImpl
\ No newline at end of file
diff --git a/duniter4j-cmd/src/main/resources/duniter4j-cmd.config b/duniter4j-cmd/src/main/resources/duniter4j-cmd.config
new file mode 100644
index 00000000..d9f4f75b
--- /dev/null
+++ b/duniter4j-cmd/src/main/resources/duniter4j-cmd.config
@@ -0,0 +1,5 @@
+duniter4j.node.host=192.168.0.5
+duniter4j.node.port=10901
+
+duniter4j.node.elasticsearch.host=localhost
+duniter4j.node.elasticsearch.port=9200
diff --git a/duniter4j-cmd/src/main/resources/log4j.properties b/duniter4j-cmd/src/main/resources/log4j.properties
new file mode 100644
index 00000000..f31940a1
--- /dev/null
+++ b/duniter4j-cmd/src/main/resources/log4j.properties
@@ -0,0 +1,27 @@
+###
+# Global logging configuration
+log4j.rootLogger=ERROR, stdout, file
+
+# Console output
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %5p - %m%n
+
+# duniter4j levels
+log4j.logger.org.duniter=INFO
+log4j.logger.org.duniter.cmd=INFO
+#log4j.logger.org.duniter.core.client.service=DEBUG
+log4j.logger.org.duniter.core.client.service.local=DEBUG
+#log4j.logger.org.duniter.core.client.service.bma=DEBUG
+log4j.logger.org.duniter.core.beans=WARN
+#log4j.logger.org.duniter.core.client.service=TRACE
+
+log4j.appender.file=org.apache.log4j.RollingFileAppender
+log4j.appender.file.file=ucoin-client.log
+log4j.appender.file.MaxFileSize=10MB
+log4j.appender.file.MaxBackupIndex=4
+
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
+log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %5p %c - %m%n
+
+
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/config/Configuration.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/config/Configuration.java
index e6d06adb..dfc6b619 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/config/Configuration.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/config/Configuration.java
@@ -84,14 +84,14 @@ public class Configuration  {
         this.applicationConfig.setEncoding(Charsets.UTF_8.name());
         this.applicationConfig.setConfigFileName(file);
 
-        // get all config providers
+        // get allOfToList config providers
         Set<ApplicationConfigProvider> providers =
                 ApplicationConfigHelper.getProviders(null,
                         null,
                         null,
                         true);
 
-        // load all default options
+        // load allOfToList default options
         ApplicationConfigHelper.loadAllDefaultOption(applicationConfig,
                 providers);
 
@@ -106,7 +106,7 @@ public class Configuration  {
         // Override application version
         initVersion(applicationConfig);
 
-        // get all transient and final option keys
+        // get allOfToList transient and final option keys
         Set<String> optionToSkip =
                 ApplicationConfigHelper.getTransientOptionKeys(providers);
 
@@ -233,11 +233,7 @@ public class Configuration  {
     public String getNodeCurrency() {
         return applicationConfig.getOption(ConfigurationOption.NODE_CURRENCY.getKey());
     }
-    
-    public String getNodeProtocol() {
-        return applicationConfig.getOption(ConfigurationOption.NODE_PROTOCOL.getKey());
-    }
-    
+
     public String getNodeHost() {
         return applicationConfig.getOption(ConfigurationOption.NODE_HOST.getKey());
     }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/config/ConfigurationOption.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/config/ConfigurationOption.java
index 41d80093..606253e0 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/config/ConfigurationOption.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/config/ConfigurationOption.java
@@ -143,14 +143,14 @@ public enum ConfigurationOption implements ConfigOptionDef {
     NODE_HOST(
             "duniter4j.node.host",
             n("duniter4j.config.option.node.host.description"),
-            "cgeek.fr",
+            "g1.duniter.org",
             String.class,
             false),
 
     NODE_PORT(
             "duniter4j.node.port",
             n("duniter4j.config.option.node.port.description"),
-            "9330",
+            "10901",
             Integer.class,
             false),
 
@@ -164,7 +164,7 @@ public enum ConfigurationOption implements ConfigOptionDef {
     NETWORK_TIMEOUT(
             "duniter4j.network.timeout",
             n("duniter4j.config.option.network.timeout.description"),
-            "100000", // = 10 s
+            "20000", // = 2 s
             Integer.class,
             false),
 
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Constants.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Constants.java
index 1df3da70..959794ae 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Constants.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Constants.java
@@ -32,4 +32,13 @@ public interface Constants {
         String CURRENCY_NAME = "[A-Za-z0-9_-]";
         String PUBKEY = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}";
     }
+
+    interface HttpStatus {
+        int SC_TOO_MANY_REQUESTS = 429;
+    }
+
+    interface Config {
+        int TOO_MANY_REQUEST_RETRY_TIME = 500; // 500 ms
+        int MAX_SAME_REQUEST_COUNT = 5; // 5 requests before to get 429 error
+    }
 }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointProtocol.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointApi.java
similarity index 92%
rename from duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointProtocol.java
rename to duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointApi.java
index 1f4b2571..3c9965ff 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointProtocol.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointApi.java
@@ -23,7 +23,10 @@ package org.duniter.core.client.model.bma;
  */
 
 
-public enum EndpointProtocol {
+public enum EndpointApi {
     BASIC_MERKLED_API,
+    BMAS,
+    ES_CORE_API,
+    ES_USER_API,
     UNDEFINED
 }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeering.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeering.java
index 1d24f9c5..560a70e0 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeering.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeering.java
@@ -35,6 +35,7 @@ public class NetworkPeering implements Serializable {
     private String block;
     private String signature;
 
+
     private String raw;
 
     private String pubkey;
@@ -112,26 +113,26 @@ public class NetworkPeering implements Serializable {
     }
 
     public static class Endpoint implements Serializable {
-        public EndpointProtocol protocol;
-        public String url;
+        public EndpointApi api;
+        public String dns;
         public String ipv4;
         public String ipv6;
         public Integer port;
 
-        public EndpointProtocol getProtocol() {
-            return protocol;
+        public EndpointApi getApi() {
+            return api;
         }
 
-        public void setProtocol(EndpointProtocol protocol) {
-            this.protocol = protocol;
+        public void setApi(EndpointApi api) {
+            this.api = api;
         }
 
-        public String getUrl() {
-            return url;
+        public String getDns() {
+            return dns;
         }
 
-        public void setUrl(String url) {
-            this.url = url;
+        public void setDns(String dns) {
+            this.dns = dns;
         }
 
         public String getIpv4() {
@@ -160,8 +161,8 @@ public class NetworkPeering implements Serializable {
 
         @Override
         public String toString() {
-            String s = "protocol=" + protocol.name() + "\n" +
-                    "url=" + url + "\n" +
+            String s = "api=" + api.name() + "\n" +
+                    "dns=" + dns + "\n" +
                     "ipv4=" + ipv4 + "\n" +
                     "ipv6=" + ipv6 + "\n" +
                     "port=" + port + "\n";
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeers.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeers.java
index 90cbf2cf..c413492d 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeers.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeers.java
@@ -148,7 +148,9 @@ public class NetworkPeers implements Serializable {
                     "status=" + status + "\n" +
                     "block=" + block + "\n";
             for(NetworkPeering.Endpoint endpoint: endpoints) {
-                s += endpoint.toString() + "\n";
+                if (endpoint != null) {
+                    s += endpoint.toString() + "\n";
+                }
             }
             return s;
         }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Protocol.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Protocol.java
index df822dd0..04c77d8f 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Protocol.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Protocol.java
@@ -27,9 +27,9 @@ package org.duniter.core.client.model.bma;
  */
 public interface Protocol {
 
-    String VERSION = "2";
+    String VERSION = "10";
 
-    String TX_VERSION = "3";
+    String TX_VERSION = "10";
 
     String TYPE_IDENTITY = "Identity";
 
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/TxSource.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/TxSource.java
index 36e4901a..4c0a05e0 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/TxSource.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/TxSource.java
@@ -89,7 +89,7 @@ public class TxSource {
 		}
 
 		/**
-		 * Source type : <ul>
+		 * Source sortType : <ul>
 		 * <li><code>D</code> : Universal Dividend</li>
 		 * <li><code>T</code> : Transaction</li>
 		 * </ul>
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/EndpointAdapter.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/EndpointAdapter.java
index 2c8d35b9..35896bd9 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/EndpointAdapter.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/EndpointAdapter.java
@@ -25,9 +25,9 @@ package org.duniter.core.client.model.bma.gson;
 import com.google.gson.TypeAdapter;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonWriter;
-import org.duniter.core.client.model.bma.EndpointProtocol;
+import org.duniter.core.client.model.bma.EndpointApi;
 import org.duniter.core.client.model.bma.NetworkPeering;
-import org.apache.http.conn.util.InetAddressUtils;
+import org.duniter.core.util.http.InetAddressUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -51,19 +51,19 @@ public class EndpointAdapter extends TypeAdapter<NetworkPeering.Endpoint> {
                     endpoint.ipv4 = word;
                 } else if (InetAddressUtils.isIPv6Address(word)) {
                     endpoint.ipv6 = word;
-                } else if (word.startsWith("http")) {
-                    endpoint.url = word;
+                } else if (word.trim().length() > 0) {
+                    endpoint.dns = word;
                 } else {
                     try {
-                        endpoint.protocol = EndpointProtocol.valueOf(word);
+                        endpoint.api = EndpointApi.valueOf(word);
                     } catch (IllegalArgumentException e) {
                         // skip this part
                     }
                 }
             }
 
-            if (endpoint.protocol == null) {
-                endpoint.protocol = EndpointProtocol.UNDEFINED;
+            if (endpoint.api == null) {
+                endpoint.api = EndpointApi.UNDEFINED;
             }
 
             return endpoint;
@@ -74,8 +74,8 @@ public class EndpointAdapter extends TypeAdapter<NetworkPeering.Endpoint> {
                 writer.nullValue();
                 return;
             }
-            writer.value(endpoint.protocol.name() + " " +
-                    endpoint.url + " " +
+            writer.value(endpoint.api.name() + " " +
+                    endpoint.dns + " " +
                     endpoint.ipv4 + " " +
                     endpoint.ipv6 + " " +
                     endpoint.port);
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/MultimapTypeAdapter.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/MultimapTypeAdapter.java
index d8b5a9b2..fbd94e04 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/MultimapTypeAdapter.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/MultimapTypeAdapter.java
@@ -58,7 +58,7 @@ public class MultimapTypeAdapter implements JsonSerializer<Multimap>, JsonDeseri
         Preconditions.checkArgument(multimapType instanceof ParameterizedType);
         final ParameterizedType paramType = (ParameterizedType)multimapType;
         final Type[] typeArguments = paramType.getActualTypeArguments();
-        Preconditions.checkArgument(2 == typeArguments.length, "Type must contain exactly 2 type arguments.");
+        Preconditions.checkArgument(2 == typeArguments.length, "Type must contain exactly 2 sortType arguments.");
 
         final ParameterizedTypeImpl valueType = new ParameterizedTypeImpl(Collection.class, null, typeArguments[1]);
         final ParameterizedTypeImpl mapType = new ParameterizedTypeImpl(Map.class, null, typeArguments[0], valueType);
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java
index 2681d5df..7a877fe6 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java
@@ -3,45 +3,124 @@ package org.duniter.core.client.model.bma.jackson;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.DeserializationContext;
 import com.fasterxml.jackson.databind.JsonDeserializer;
-import org.apache.http.conn.util.InetAddressUtils;
-import org.duniter.core.client.model.bma.EndpointProtocol;
+import org.duniter.core.client.model.bma.EndpointApi;
 import org.duniter.core.client.model.bma.NetworkPeering;
+import org.duniter.core.util.StringUtils;
+import org.duniter.core.util.http.InetAddressUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Created by blavenie on 07/12/16.
  */
 public class EndpointDeserializer extends JsonDeserializer<NetworkPeering.Endpoint> {
+
+    private static final Logger log = LoggerFactory.getLogger(EndpointDeserializer.class);
+
+    public static final String EP_END_REGEXP = "(?:[ ]+([a-z0-9-_]+[.][a-z0-9-_.]*))?(?:[ ]+([0-9.]+))?(?:[ ]+([0-9a-f:]+))?(?:[ ]+([0-9]+))$";
+    public static final String BMA_API_REGEXP = "^BASIC_MERKLED_API" + EP_END_REGEXP;
+    public static final String BMAS_API_REGEXP = "^BMAS" + EP_END_REGEXP;
+    public static final String OTHER_API_REGEXP = "^([A-Z_-]+)" + EP_END_REGEXP;
+
+    private Pattern bmaPattern;
+    private Pattern bmasPattern;
+    private Pattern otherApiPattern;
+
+    public EndpointDeserializer() {
+        bmaPattern = Pattern.compile(BMA_API_REGEXP);
+        bmasPattern = Pattern.compile(BMAS_API_REGEXP);
+        otherApiPattern = Pattern.compile(OTHER_API_REGEXP);
+    }
+
     @Override
     public NetworkPeering.Endpoint deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
 
         String ept = jp.getText();
-        ArrayList<String> parts = new ArrayList<>(Arrays.asList(ept.split(" ")));
+
         NetworkPeering.Endpoint endpoint = new NetworkPeering.Endpoint();
-        endpoint.port = Integer.parseInt(parts.remove(parts.size() - 1));
-        for (String word : parts) {
-            if (InetAddressUtils.isIPv4Address(word)) {
-                endpoint.ipv4 = word;
-            } else if (InetAddressUtils.isIPv6Address(word)) {
-                endpoint.ipv6 = word;
-            } else if (word.startsWith("http")) {
-                endpoint.url = word;
-            } else {
-                try {
-                    endpoint.protocol = EndpointProtocol.valueOf(word);
-                } catch (IllegalArgumentException e) {
-                    // skip this part
+
+        // BMA API
+        Matcher mather = bmaPattern.matcher(ept);
+        if (mather.matches()) {
+            endpoint.api = EndpointApi.BASIC_MERKLED_API;
+
+            for(int i=1; i<=mather.groupCount(); i++) {
+                String word = mather.group(i);
+
+                if (StringUtils.isNotBlank(word)) {
+                    if (InetAddressUtils.isIPv4Address(word)) {
+                        endpoint.ipv4 = word;
+                    } else if (InetAddressUtils.isIPv6Address(word)) {
+                        endpoint.ipv6 = word;
+                    } else if (i == mather.groupCount() && word.matches("\\d+")){
+                        endpoint.port = Integer.parseInt(word);
+                    } else {
+                        endpoint.dns = word;
+                    }
+                }
+            }
+
+            return endpoint;
+        }
+
+        // BMAS API
+        mather = bmasPattern.matcher(ept);
+        if (mather.matches()) {
+            endpoint.api = EndpointApi.BMAS;
+
+            for(int i=1; i<=mather.groupCount(); i++) {
+                String word = mather.group(i);
+
+                if (StringUtils.isNotBlank(word)) {
+                    if (InetAddressUtils.isIPv4Address(word)) {
+                        endpoint.ipv4 = word;
+                    } else if (InetAddressUtils.isIPv6Address(word)) {
+                        endpoint.ipv6 = word;
+                    } else if (i == mather.groupCount() && word.matches("\\d+")){
+                        endpoint.port = Integer.parseInt(word);
+                    } else {
+                        endpoint.dns = word;
+                    }
                 }
             }
+
+            return endpoint;
         }
 
-        if (endpoint.protocol == null) {
-            endpoint.protocol = EndpointProtocol.UNDEFINED;
+        // Other API
+        mather = otherApiPattern.matcher(ept);
+        if (mather.matches()) {
+            try {
+                endpoint.api = EndpointApi.valueOf(mather.group(1));
+            } catch(Exception e) {
+                log.warn("Unable to deserialize endpoint: unknown api [" + mather.group(1) + "]");
+                // not known API: skip
+                return null;
+            }
+
+            for(int i=2; i<=mather.groupCount(); i++) {
+                String word = mather.group(i);
+
+                if (StringUtils.isNotBlank(word)) {
+                    if (InetAddressUtils.isIPv4Address(word)) {
+                        endpoint.ipv4 = word;
+                    } else if (InetAddressUtils.isIPv6Address(word)) {
+                        endpoint.ipv6 = word;
+                    } else if (i == mather.groupCount() && word.matches("\\d+")){
+                        endpoint.port = Integer.parseInt(word);
+                    } else {
+                        endpoint.dns = word;
+                    }
+                }
+            }
+
+            return endpoint;
         }
 
-        return endpoint;
+        return null;
     }
 }
\ No newline at end of file
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/DeleteRecord.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/DeleteRecord.java
index c09fe0d1..d87291d7 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/DeleteRecord.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/DeleteRecord.java
@@ -28,7 +28,7 @@ package org.duniter.core.client.model.elasticsearch;
 public class DeleteRecord extends Record {
 
     public static final String PROPERTY_INDEX="index";
-    public static final String PROPERTY_TYPE="type";
+    public static final String PROPERTY_TYPE="sortType";
     public static final String PROPERTY_ID="id";
 
     private String index;
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peer.java
index 5b292b87..02eb05cc 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peer.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peer.java
@@ -23,107 +23,460 @@ package org.duniter.core.client.model.local;
  */
 
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.duniter.core.client.model.bma.EndpointApi;
+import org.duniter.core.client.model.bma.NetworkPeering;
+import org.duniter.core.util.Preconditions;
+import org.duniter.core.util.StringUtils;
+import org.duniter.core.util.http.InetAddressUtils;
+
 import java.io.Serializable;
+import java.util.StringJoiner;
 
 public class Peer implements LocalEntity, Serializable {
 
+
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+
+        private String api;
+        private String dns;
+        private String ipv4;
+        private String ipv6;
+        private Integer port;
+        private Boolean useSsl;
+        private String pubkey;
+        private String hash;
+        private String currency;
+
+        public Builder() {
+
+        }
+
+        public Builder setApi(String api) {
+            this.api = api;
+            return this;
+        }
+
+        public Builder setDns(String dns) {
+            this.dns = dns;
+            return this;
+        }
+
+        public Builder setIpv4(String ipv4) {
+            this.ipv4 = ipv4;
+            return this;
+        }
+
+        public Builder setIpv6(String ipv6) {
+            this.ipv6 = ipv6;
+            return this;
+        }
+
+        public Builder setPort(int port) {
+            this.port = port;
+            return this;
+        }
+
+        public Builder setUseSsl(boolean useSsl) {
+            this.useSsl = useSsl;
+            return this;
+        }
+
+        public Builder setCurrency(String currency) {
+            this.currency = currency;
+            return this;
+        }
+
+        public Builder setPubkey(String pubkey) {
+            this.pubkey = pubkey;
+            return this;
+        }
+
+        public Builder setHash(String hash) {
+            this.hash = hash;
+            return this;
+        }
+
+        public Builder setHost(String host) {
+            Preconditions.checkNotNull(host);
+            if (InetAddressUtils.isIPv4Address(host)) {
+                this.ipv4 = host;
+            }
+            else if (InetAddressUtils.isIPv6Address(host)) {
+                this.ipv6 = host;
+            }
+            else {
+                this.dns = host;
+            }
+            return this;
+        }
+
+        public Builder setEndpoint(NetworkPeering.Endpoint source) {
+            Preconditions.checkNotNull(source);
+            if (source.api != null) {
+               setApi(source.api.name());
+            }
+            if (StringUtils.isNotBlank(source.dns)) {
+               setDns(source.dns);
+            }
+            if (StringUtils.isNotBlank(source.ipv4)) {
+               setIpv4(source.ipv4);
+            }
+            if (StringUtils.isNotBlank(source.ipv6)) {
+               setIpv6(source.ipv6);
+            }
+            if (StringUtils.isNotBlank(source.ipv6)) {
+               setHost(source.ipv6);
+            }
+            if (source.port != null) {
+               setPort(source.port);
+            }
+            return this;
+        }
+
+        public Peer build() {
+            int port = this.port != null ? this.port : 80;
+            boolean useSsl = this.useSsl != null ? this.useSsl :
+                    (port == 443 || this.api == EndpointApi.BMAS.name());
+            String api = this.api != null ? this.api : EndpointApi.BASIC_MERKLED_API.name();
+            Peer ep = new Peer(api, dns, ipv4, ipv6, port, useSsl);
+            if (StringUtils.isNotBlank(this.currency)) {
+                ep.setCurrency(this.currency);
+            }
+            if (StringUtils.isNotBlank(this.pubkey)) {
+                ep.setPubkey(this.pubkey);
+            }
+            if (StringUtils.isNotBlank(this.hash)) {
+                ep.setHash(this.hash);
+            }
+            return ep;
+        }
+
+    }
+
+
+    // Local entity attribute (only used for local DB)
     private Long id;
     private Long currencyId;
+
+    private String api;
+    private String dns;
+    private String ipv4;
+    private String ipv6;
+
+    private String url;
     private String host;
+    private String pubkey;
+
+    private String hash;
+    private String currency;
+
+    private Stats stats = new Stats();
+
     private int port;
     private boolean useSsl;
-    private String url; // computed
 
     public Peer() {
         // default constructor, need for de-serialization
     }
 
-    public Peer(String host, int port) {
-        this.host = host;
-        this.port = port;
-        this.useSsl = (port == 443);
-        this.url = computeUrl(this.host, this.port, this.useSsl);
+    /**
+     * @deprecated Use Builder instead
+     * @param host Can be a ipv4, ipv6 or a dns
+     * @param port any port, or null (default: 80)
+     */
+    @Deprecated
+    public Peer(String host, Integer port) {
+        this.api = EndpointApi.BASIC_MERKLED_API.name();
+        if (InetAddressUtils.isIPv4Address(host)) {
+            this.ipv4 = host;
+        }
+        if (InetAddressUtils.isIPv6Address(host)) {
+            this.ipv6 = host;
+        }
+        else {
+            this.dns = host;
+        }
+        this.port = port != null ? port : 80;
+        this.useSsl = (port == 443 || this.api == EndpointApi.BMAS.name());
+        init();
     }
 
-    public Peer(String host, int port, boolean useSsl) {
-        this.host = host;
+    public Peer(String api, String dns, String ipv4, String ipv6, int port, boolean useSsl) {
+        this.api = api;
+        this.dns = StringUtils.isNotBlank(dns) ? dns : null;
+        this.ipv4 = StringUtils.isNotBlank(ipv4) ? ipv4 : null;
+        this.ipv6 = StringUtils.isNotBlank(ipv6) ? ipv6 : null;
         this.port = port;
         this.useSsl = useSsl;
-        this.url = computeUrl(this.host, this.port, this.useSsl);
+        init();
     }
 
-    public String getHost() {
-        return host;
-    }
-
-    public int getPort() {
-        return port;
-    }
-
-    public String getUrl() {
-        return url;
+    protected void init() {
+        // If SSL: prefer DNS name (should be defined in SSL certificate)
+        // else (if define) use ipv4 (if NOT local IP)
+        // else (if define) use dns
+        // else (if define) use ipv6
+        this.host = ((port == 443 || useSsl) && dns != null) ? dns :
+                (ipv4 != null && InetAddressUtils.isNotLocalIPv4Address(ipv4) ? ipv4 :
+                    (dns != null ? dns :
+                        (ipv6 != null ? "[" + ipv6 + "]" : "")));
+        String protocol = (port == 443 || useSsl) ? "https" : "http";
+        this.url = protocol + "://" + this.host + (port != 80 ? (":" + port) : "");
     }
 
+    @JsonIgnore
     public Long getId() {
         return id;
     }
 
+    @JsonIgnore
     public void setId(Long id) {
         this.id = id;
     }
 
+    @JsonIgnore
     public Long getCurrencyId() {
         return currencyId;
     }
 
+    @JsonIgnore
     public void setCurrencyId(Long currencyId) {
         this.currencyId = currencyId;
     }
 
-    public void setPort(int port) {
-        this.port = port;
-        if (port == 443) {
-            this.useSsl = true;
-        }
-        this.url = computeUrl(this.host, this.port, this.useSsl);
+    public String getApi() {
+        return api;
     }
 
-    public void setHost(String host) {
-        this.host = host;
-        this.url = computeUrl(this.host, this.port, this.useSsl);
+    public String getDns() {
+        return dns;
+    }
+
+    public void setDns(String dns) {
+        this.dns = dns;
+        init();
+    }
+
+    public String getIpv4() {
+        return ipv4;
+    }
+
+    public void setIpv4(String ipv4) {
+        this.ipv4 = ipv4;
+        init();
+    }
+
+    public String getIpv6() {
+        return ipv6;
+    }
+
+    public void setIpv6(String ipv6) {
+        this.ipv6 = ipv6;
+        init();
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+        init();
     }
 
     public boolean isUseSsl() {
-        return this.useSsl;
+        return useSsl;
     }
 
     public void setUseSsl(boolean useSsl) {
         this.useSsl = useSsl;
-        this.url = computeUrl(this.host, this.port, this.useSsl);
+        init();
     }
 
-    public String toString() {
-        return new StringBuilder().append(host)
-                .append(":")
-                .append(port)
-                .append(useSsl ? "[+SSL]" : "")
-                .toString();
+    public String getPubkey() {
+        return pubkey;
+    }
+
+    public void setPubkey(String pubkey) {
+        this.pubkey = pubkey;
+    }
+
+    @JsonIgnore
+    public String getHost() {
+        return this.host; // computed in init()
+    }
+
+    @JsonIgnore
+    public String getUrl() {
+        return this.url; // computed in init()
+    }
+
+    public String getHash() {
+        return hash;
+    }
+
+    public void setHash(String hash) {
+        this.hash = hash;
+    }
+
+    public String getCurrency() {
+        return currency;
+    }
+
+    public void setCurrency(String currency) {
+        this.currency = currency;
+    }
+
+    @JsonIgnore
+    public Stats getStats() {
+        return stats;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (o == null) {
-            return false;
+    @JsonIgnore
+    public void setStats(Stats stats) {
+        this.stats = stats;
+    }
+
+    public String toString() {
+        StringJoiner joiner = new StringJoiner(" ");
+        if (api != null) {
+            joiner.add(api);
+        }
+        if (dns != null) {
+            joiner.add(dns);
+        }
+        if (ipv4 != null) {
+            joiner.add(ipv4);
         }
-        if (id != null && o instanceof Peer) {
-            return id.equals(((Peer)o).getId());
+        if (ipv6 != null) {
+            joiner.add(ipv6);
         }
-        return super.equals(o);
+        if (port != 80) {
+            joiner.add(String.valueOf(port));
+        }
+        return joiner.toString();
+    }
+
+    public enum PeerStatus {
+        UP,
+        DOWN,
+        ERROR
     }
 
-    /* -- Internal methods -- */
+    public static class Stats {
+        private String version;
+        private PeerStatus status = PeerStatus.UP; // default
+        private Integer blockNumber;
+        private String blockHash;
+        private String error;
+        private Long medianTime;
+        private Integer hardshipLevel;
+        private boolean isMainConsensus = false;
+        private boolean isForkConsensus = false;
+        private Double consensusPct = 0d;
+        private String uid;
+
+        public Stats() {
+
+        }
+
+        public PeerStatus getStatus() {
+            return status;
+        }
+
+        @JsonIgnore
+        public boolean isReacheable() {
+            return status != null && status == PeerStatus.UP;
+        }
+
+        public void setStatus(PeerStatus status) {
+            this.status = status;
+        }
+
+        public String getError() {
+            return error;
+        }
 
-    protected String computeUrl(String host, int port, boolean useSsl) {
-        return String.format("%s://%s:%s", (useSsl ? "https" : "http"), host, port);
+        public void setError(String error) {
+            this.error = error;
+        }
+
+        public String getVersion() {
+            return version;
+        }
+
+        public void setVersion(String version) {
+            this.version = version;
+        }
+
+        public Integer getBlockNumber() {
+            return blockNumber;
+        }
+
+        public void setBlockNumber(Integer blockNumber) {
+            this.blockNumber = blockNumber;
+        }
+
+        public String getBlockHash() {
+            return blockHash;
+        }
+
+        public void setBlockHash(String blockHash) {
+            this.blockHash = blockHash;
+        }
+
+        public Long getMedianTime() {
+            return medianTime;
+        }
+
+        public void setMedianTime(Long medianTime) {
+            this.medianTime = medianTime;
+        }
+
+        public boolean isMainConsensus() {
+            return isMainConsensus;
+        }
+
+        public void setMainConsensus(boolean mainConsensus) {
+            this.isMainConsensus = mainConsensus;
+        }
+
+        public boolean isForkConsensus() {
+            return isForkConsensus;
+        }
+
+        public void setForkConsensus(boolean forkConsensus) {
+            this.isForkConsensus = forkConsensus;
+        }
+
+        public Double getConsensusPct() {
+            return consensusPct;
+        }
+
+        public void setConsensusPct(Double consensusPct) {
+            this.consensusPct = consensusPct;
+        }
+
+        public Integer getHardshipLevel() {
+            return hardshipLevel;
+        }
+
+        public void setHardshipLevel(Integer hardshipLevel) {
+            this.hardshipLevel = hardshipLevel;
+        }
+
+        public String getUid() {
+            return uid;
+        }
+
+        public void setUid(String uid) {
+            this.uid = uid;
+        }
     }
 }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java
index ac46d141..eaa8ce61 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java
@@ -24,7 +24,6 @@ package org.duniter.core.client.service;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Joiner;
-import org.apache.commons.io.IOUtils;
 import org.apache.http.HttpResponse;
 import org.apache.http.HttpStatus;
 import org.apache.http.client.HttpClient;
@@ -38,13 +37,13 @@ import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
 import org.duniter.core.beans.InitializingBean;
 import org.duniter.core.client.config.Configuration;
+import org.duniter.core.client.model.bma.Constants;
 import org.duniter.core.client.model.bma.Error;
 import org.duniter.core.client.model.bma.jackson.JacksonUtils;
 import org.duniter.core.client.model.local.Peer;
 import org.duniter.core.client.service.exception.*;
 import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.util.StringUtils;
-import org.duniter.core.util.cache.Cache;
 import org.duniter.core.util.cache.SimpleCache;
 import org.nuiton.i18n.I18n;
 import org.slf4j.Logger;
@@ -93,6 +92,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
     protected void initCaches() {
         Configuration config = Configuration.instance();
         int cacheTimeInMillis = config.getNetworkCacheTimeInMillis();
+        int defaultTimeout = config.getNetworkTimeout();
 
         requestConfigCache = new SimpleCache<Integer, RequestConfig>(cacheTimeInMillis) {
             @Override
@@ -162,7 +162,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
     }
 
     public <T> T executeRequest(Peer peer, String absolutePath, Class<? extends T> resultClass)  {
-        HttpGet httpGet = new HttpGet(getPath(peer, absolutePath));
+        HttpGet httpGet = new HttpGet(peer.getUrl() + absolutePath);
         return executeRequest(httpClientCache.get(0), httpGet, resultClass);
     }
 
@@ -228,12 +228,17 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
 
     @SuppressWarnings("unchecked")
     protected <T> T executeRequest(HttpClient httpClient, HttpUriRequest request, Class<? extends T> resultClass, Class<?> errorClass)  {
+        return executeRequest(httpClient, request, resultClass, errorClass, 5);
+    }
+
+    protected <T> T executeRequest(HttpClient httpClient, HttpUriRequest request, Class<? extends T> resultClass, Class<?> errorClass, int retryCount)  {
         T result = null;
 
         if (debug) {
             log.debug("Executing request : " + request.getRequestLine());
         }
 
+        boolean retry = false;
         HttpResponse response = null;
         try {
             response = httpClient.execute(request);
@@ -271,6 +276,11 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
                     catch(IOException e) {
                         throw new HttpBadRequestException(I18n.t("duniter4j.client.status", response.getStatusLine().toString()));
                     }
+
+                case HttpStatus.SC_SERVICE_UNAVAILABLE:
+                case Constants.HttpStatus.SC_TOO_MANY_REQUESTS:
+                    retry = true;
+                    break;
                 default:
                     throw new TechnicalException(I18n.t("duniter4j.client.status", request.toString(), response.getStatusLine().toString()));
             }
@@ -296,6 +306,23 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
             }
         }
 
+        // HTTP requests limit exceed, retry when possible
+        if (retry) {
+            if (retryCount > 0) {
+                log.debug(String.format("Service unavailable: waiting [%s ms] before retrying...", Constants.Config.TOO_MANY_REQUEST_RETRY_TIME));
+                try {
+                    Thread.sleep(Constants.Config.TOO_MANY_REQUEST_RETRY_TIME);
+                } catch (InterruptedException e) {
+                    throw new TechnicalException(I18n.t("duniter4j.client.status", request.toString(), response.getStatusLine().toString()));
+                }
+                // iterate
+                return executeRequest(httpClient, request, resultClass, errorClass, retryCount - 1);
+            }
+            else {
+                throw new TechnicalException(I18n.t("duniter4j.client.status", request.toString(), response.getStatusLine().toString()));
+            }
+        }
+
         return result;
     }
 
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/ServiceLocator.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/ServiceLocator.java
index e0f14a35..38107551 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/ServiceLocator.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/ServiceLocator.java
@@ -25,9 +25,13 @@ package org.duniter.core.client.service;
 
 import org.duniter.core.beans.Bean;
 import org.duniter.core.beans.BeanFactory;
-import org.duniter.core.client.service.bma.*;
+import org.duniter.core.client.service.bma.BlockchainRemoteService;
+import org.duniter.core.client.service.bma.NetworkRemoteService;
+import org.duniter.core.client.service.bma.TransactionRemoteService;
+import org.duniter.core.client.service.bma.WotRemoteService;
 import org.duniter.core.client.service.elasticsearch.CurrencyRegistryRemoteService;
 import org.duniter.core.client.service.local.CurrencyService;
+import org.duniter.core.client.service.local.NetworkService;
 import org.duniter.core.client.service.local.PeerService;
 import org.duniter.core.service.CryptoService;
 import org.duniter.core.service.MailService;
@@ -140,10 +144,14 @@ public class ServiceLocator implements Closeable {
         return getBean(CurrencyRegistryRemoteService.class);
     }
 
-    public MailService getMaiLService() {
+    public MailService getMailService() {
         return getBean(MailService.class);
     }
 
+    public NetworkService getNetworkService() {
+        return getBean(NetworkService.class);
+    }
+
     public <S extends Bean> S getBean(Class<S> clazz) {
         if (beanFactory == null) {
             initBeanFactory();
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteService.java
index 8fb4bf2b..8e92845e 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteService.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteService.java
@@ -76,7 +76,7 @@ public interface BlockchainRemoteService extends Service {
 
     /**
      * Retrieve the dividend of a block, by id (from 0 to current).
-     * Usefull method to avoid to deserialize all the block
+     * Usefull method to avoid to deserialize allOfToList the block
      *
      * @param currencyId
      * @param number
@@ -222,8 +222,24 @@ public interface BlockchainRemoteService extends Service {
      */
     Map<Integer, Long> getUDs(long currencyId, long startOffset);
 
+    /**
+     * Listening new block event
+     * @param currencyId
+     * @param listener
+     * @return
+     */
     WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener);
 
     WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener);
 
+    /**
+     * Listening new peer event
+     * @param currencyId
+     * @param listener
+     * @return
+     */
+    WebsocketClientEndpoint addPeerListener(long currencyId, WebsocketClientEndpoint.MessageListener listener);
+
+    WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener);
+
 }
\ No newline at end of file
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceImpl.java
index d431342e..25c8b7a1 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceImpl.java
@@ -22,6 +22,7 @@ package org.duniter.core.client.service.bma;
  * #L%
  */
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.duniter.core.util.Preconditions;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
@@ -76,6 +77,11 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
 
     public static final String URL_MEMBERSHIP_SEARCH = URL_BASE + "/memberships/%s";
 
+    public static final String URL_WS_BLOCK = "/ws/block";
+
+    public static final String URL_WS_PEER = "/ws/peer";
+
+    private ObjectMapper objectMapper;
 
     private NetworkRemoteService networkRemoteService;
 
@@ -88,7 +94,7 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
     // Cache on blockchain parameters
     private Cache<Long, BlockchainParameters> mParametersCache;
 
-    private Map<URI, WebsocketClientEndpoint> blockWsEndPoints = new HashMap<>();
+    private Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>();
 
     public BlockchainRemoteServiceImpl() {
         super();
@@ -108,11 +114,11 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
     public void close() throws IOException {
         super.close();
 
-        if (blockWsEndPoints.size() != 0) {
-            for (WebsocketClientEndpoint clientEndPoint: blockWsEndPoints.values()) {
+        if (wsEndPoints.size() != 0) {
+            for (WebsocketClientEndpoint clientEndPoint: wsEndPoints.values()) {
                 clientEndPoint.close();
             }
-            blockWsEndPoints.clear();
+            wsEndPoints.clear();
         }
     }
 
@@ -561,8 +567,7 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
 
     @Override
     public WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener) {
-        Peer peer = peerService.getActivePeerByCurrencyId(currencyId);
-        return addBlockListener(peer, listener);
+        return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener);
     }
 
     @Override
@@ -570,8 +575,27 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
         Preconditions.checkNotNull(peer);
         Preconditions.checkNotNull(listener);
 
-        // Get the websocket endpoint
-        WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, "/ws/block");
+        // Get (or create) the websocket endpoint
+        WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_BLOCK);
+
+        // add listener
+        wsClientEndPoint.registerListener(listener);
+
+        return wsClientEndPoint;
+    }
+
+    @Override
+    public WebsocketClientEndpoint addPeerListener(long currencyId, WebsocketClientEndpoint.MessageListener listener) {
+        return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener);
+    }
+
+    @Override
+    public WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener) {
+        Preconditions.checkNotNull(peer);
+        Preconditions.checkNotNull(listener);
+
+        // Get (or create) the websocket endpoint
+        WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_PEER);
 
         // add listener
         wsClientEndPoint.registerListener(listener);
@@ -807,17 +831,17 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
                     path));
 
             // Get the websocket, or open new one if not exists
-            WebsocketClientEndpoint wsClientEndPoint = blockWsEndPoints.get(wsBlockURI);
+            WebsocketClientEndpoint wsClientEndPoint = wsEndPoints.get(wsBlockURI);
             if (wsClientEndPoint == null || wsClientEndPoint.isClosed()) {
-                log.info(String.format("Starting to listen block from [%s]...", wsBlockURI.toString()));
+                log.info(String.format("Starting to listen on [%s]...", wsBlockURI.toString()));
                 wsClientEndPoint = new WebsocketClientEndpoint(wsBlockURI);
-                blockWsEndPoints.put(wsBlockURI, wsClientEndPoint);
+                wsEndPoints.put(wsBlockURI, wsClientEndPoint);
             }
 
             return wsClientEndPoint;
 
         } catch (URISyntaxException | ServiceConfigurationError ex) {
-            throw new TechnicalException("could not create URI need for web socket on block: " + ex.getMessage());
+            throw new TechnicalException(String.format("Could not create URI need for web socket [%s]: %s", path, ex.getMessage()));
         }
 
     }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java
index 9e5da699..262dd501 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java
@@ -23,9 +23,11 @@ package org.duniter.core.client.service.bma;
  */
 
 import org.duniter.core.beans.Service;
-import org.duniter.core.client.model.bma.EndpointProtocol;
+import org.duniter.core.client.model.bma.EndpointApi;
 import org.duniter.core.client.model.bma.NetworkPeering;
+import org.duniter.core.client.model.bma.NetworkPeers;
 import org.duniter.core.client.model.local.Peer;
+import org.duniter.core.util.websocket.WebsocketClientEndpoint;
 
 import java.util.List;
 
@@ -38,5 +40,11 @@ public interface NetworkRemoteService extends Service {
 
     List<Peer> getPeers(Peer peer);
 
-    List<Peer> findPeers(Peer peer, String status, EndpointProtocol endpointProtocol, Integer currentBlockNumber, String currentBlockHash);
+    List<String> getPeersLeaves(Peer peer);
+
+    NetworkPeers.Peer getPeerLeaf(Peer peer, String leaf);
+
+    List<Peer> findPeers(Peer peer, String status, EndpointApi endpointApi, Integer currentBlockNumber, String currentBlockHash);
+
+    WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect);
 }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java
index 48902b02..3815be7e 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java
@@ -22,22 +22,31 @@ package org.duniter.core.client.service.bma;
  * #L%
  */
 
-import java.util.ArrayList;
-import java.util.List;
-
-import org.duniter.core.client.model.bma.EndpointProtocol;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.duniter.core.client.model.bma.EndpointApi;
 import org.duniter.core.client.model.bma.NetworkPeering;
 import org.duniter.core.client.model.bma.NetworkPeers;
+import org.duniter.core.client.model.bma.jackson.JacksonUtils;
 import org.duniter.core.client.model.local.Peer;
-import org.duniter.core.util.ObjectUtils;
+import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.util.Preconditions;
 import org.duniter.core.util.StringUtils;
+import org.duniter.core.util.websocket.WebsocketClientEndpoint;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Created by eis on 05/02/15.
  */
 public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements NetworkRemoteService{
 
+    private static final Logger log = LoggerFactory.getLogger(NetworkRemoteServiceImpl.class);
 
     public static final String URL_BASE = "/network";
 
@@ -47,10 +56,20 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N
 
     public static final String URL_PEERING_PEERS = URL_PEERING + "/peers";
 
-    public static final String URL_PEERING_PEERS_LEAF = URL_PEERING + "/peers?leaf=";
+    public static final String URL_PEERING_PEERS_LEAVES = URL_PEERING_PEERS + "?leaves=true";
+
+    public static final String URL_PEERING_PEERS_LEAF = URL_PEERING_PEERS + "?leaf=";
+
+    public static final String URL_WS_PEER = "/ws/peer";
+
+    protected ObjectMapper objectMapper;
+
+    private Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>();
 
     public NetworkRemoteServiceImpl() {
         super();
+
+        objectMapper = JacksonUtils.newObjectMapper();
     }
 
     public NetworkPeering getPeering(Peer peer) {
@@ -58,13 +77,60 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N
         return result;
     }
 
+    @Override
+    public void close() throws IOException {
+        super.close();
+
+        if (wsEndPoints.size() != 0) {
+            for (WebsocketClientEndpoint clientEndPoint: wsEndPoints.values()) {
+                clientEndPoint.close();
+            }
+            wsEndPoints.clear();
+        }
+    }
+
     @Override
     public List<Peer> getPeers(Peer peer) {
         return findPeers(peer, null, null, null, null);
     }
 
     @Override
-    public List<Peer> findPeers(Peer peer, String status, EndpointProtocol endpointProtocol, Integer currentBlockNumber, String currentBlockHash) {
+    public List<String> getPeersLeaves(Peer peer) {
+        Preconditions.checkNotNull(peer);
+
+        List<String> result = new ArrayList<>();
+        JsonNode jsonNode= httpService.executeRequest(peer, URL_PEERING_PEERS_LEAVES, JsonNode.class);
+        jsonNode.get("leaves").forEach(jsonNode1 -> {
+            result.add(jsonNode1.asText());
+        });
+        return result;
+    }
+
+    @Override
+    public NetworkPeers.Peer getPeerLeaf(Peer peer, String leaf) {
+        Preconditions.checkNotNull(peer);
+        JsonNode jsonNode = httpService.executeRequest(peer, URL_PEERING_PEERS_LEAF + leaf, JsonNode.class);
+        NetworkPeers.Peer result = null;
+
+        try {
+
+            if (jsonNode.has("leaf")) {
+                jsonNode = jsonNode.get("leaf");
+                if (jsonNode.has("value")) {
+                    jsonNode = jsonNode.get("value");
+                    String json = objectMapper.writeValueAsString(jsonNode);
+                    result = objectMapper.readValue(json, NetworkPeers.Peer.class);
+                }
+            }
+        } catch(IOException e) {
+            throw new TechnicalException(e);
+        }
+
+        return result;
+    }
+
+    @Override
+    public List<Peer> findPeers(Peer peer, String status, EndpointApi endpointApi, Integer currentBlockNumber, String currentBlockHash) {
         Preconditions.checkNotNull(peer);
 
         List<Peer> result = new ArrayList<Peer>();
@@ -80,13 +146,15 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N
 
                 for (NetworkPeering.Endpoint endpoint : remotePeer.endpoints) {
 
-                    match = endpointProtocol == null || endpointProtocol == endpoint.protocol;
+                    match = endpointApi == null || endpointApi == endpoint.api;
 
-                    if (match) {
-                        Peer childPeer = toPeer(endpoint);
-                        if (childPeer != null) {
-                            result.add(childPeer);
-                        }
+                    if (match && endpoint != null) {
+                        Peer childPeer = Peer.newBuilder()
+                                .setCurrency(remotePeer.getCurrency())
+                                .setPubkey(remotePeer.getPubkey())
+                                .setEndpoint(endpoint)
+                                .build();
+                        result.add(childPeer);
                     }
 
                 }
@@ -96,25 +164,22 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N
         return result;
     }
 
-    /* -- Internal methods -- */
+    @Override
+    public WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) {
+        Preconditions.checkNotNull(peer);
+        Preconditions.checkNotNull(listener);
 
-    protected Peer toPeer(NetworkPeering.Endpoint source) {
-        Peer target = new Peer();
-        if (StringUtils.isNotBlank(source.ipv4)) {
-            target.setHost(source.ipv4);
-        } else if (StringUtils.isNotBlank(source.ipv6)) {
-            target.setHost(source.ipv6);
-        } else if (StringUtils.isNotBlank(source.url)) {
-            target.setHost(source.url);
-        } else {
-            target = null;
-        }
-        if (target != null && source.port != null) {
-            target.setPort(source.port);
-        }
-        return target;
+        // Get (or create) the websocket endpoint
+        WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_PEER, autoReconnect);
+
+        // add listener
+        wsClientEndPoint.registerListener(listener);
+
+        return wsClientEndPoint;
     }
 
+    /* -- Internal methods -- */
+
     protected Integer parseBlockNumber(NetworkPeers.Peer remotePeer) {
         Preconditions.checkNotNull(remotePeer);
 
@@ -148,4 +213,29 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N
         String hash = remotePeer.block.substring(index+1);
         return hash;
     }
+
+    public WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path, boolean autoReconnect) {
+
+        try {
+            URI wsBlockURI = new URI(String.format("%s://%s:%s%s",
+                    peer.isUseSsl() ? "wss" : "ws",
+                    peer.getHost(),
+                    peer.getPort(),
+                    path));
+
+            // Get the websocket, or open new one if not exists
+            WebsocketClientEndpoint wsClientEndPoint = wsEndPoints.get(wsBlockURI);
+            if (wsClientEndPoint == null || wsClientEndPoint.isClosed()) {
+                log.info(String.format("Starting to listen on [%s]...", wsBlockURI.toString()));
+                wsClientEndPoint = new WebsocketClientEndpoint(wsBlockURI, autoReconnect);
+                wsEndPoints.put(wsBlockURI, wsClientEndPoint);
+            }
+
+            return wsClientEndPoint;
+
+        } catch (URISyntaxException | ServiceConfigurationError ex) {
+            throw new TechnicalException(String.format("Could not create URI need for web socket [%s]: %s", path, ex.getMessage()));
+        }
+
+    }
 }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/TransactionRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/TransactionRemoteServiceImpl.java
index b927f4e5..a6ad175c 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/TransactionRemoteServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/TransactionRemoteServiceImpl.java
@@ -308,7 +308,9 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen
 		}
 
 		// Comment
-		sb.append("Comment: ").append(comments).append('\n');
+		sb.append("Comment: ");
+		if (comments != null) sb.append(comments);
+		sb.append('\n');
 
 		return sb.toString();
 	}
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteService.java
index 09fbe9c2..0b8aca4c 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteService.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteService.java
@@ -32,6 +32,7 @@ import org.duniter.core.client.model.local.Wallet;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public interface WotRemoteService extends Service {
@@ -64,6 +65,10 @@ public interface WotRemoteService extends Service {
 
     String getSignedIdentity(String currency, byte[] pubKey, byte[] secKey, String uid, String blockUid);
 
+    Map<String, String> getMembersUids(long currencyId);
+
+    Map<String, String> getMembersUids(Peer peer);
+
     void sendIdentity(long currencyId, byte[] pubKey, byte[] secKey, String uid, String blockUid);
 
     void sendIdentity(Peer peer, String currency, byte[] pubKey, byte[] secKey, String uid, String blockUid);
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteServiceImpl.java
index 3e42c346..863e6e07 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteServiceImpl.java
@@ -22,6 +22,7 @@ package org.duniter.core.client.service.bma;
  * #L%
  */
 
+import com.fasterxml.jackson.databind.JsonNode;
 import org.duniter.core.client.model.ModelUtils;
 import org.duniter.core.client.model.bma.*;
 import org.duniter.core.client.model.local.Certification;
@@ -54,6 +55,8 @@ public class WotRemoteServiceImpl extends BaseRemoteServiceImpl implements WotRe
 
     public static final String URL_ADD = URL_BASE + "/add";
 
+    public static final String URL_MEMBERS = URL_BASE + "/members";
+
     public static final String URL_LOOKUP = URL_BASE + "/lookup/%s";
 
     public static final String URL_REQUIREMENT = URL_BASE+"/requirements/%s";
@@ -119,6 +122,35 @@ public class WotRemoteServiceImpl extends BaseRemoteServiceImpl implements WotRe
 
     }
 
+    public Map<String, String> getMembersUids(long currencyId) {
+        // get /wot/members
+        JsonNode json = executeRequest(currencyId, URL_MEMBERS, JsonNode.class);
+
+        if (json == null  || !json.has("results")) return null;
+
+        Map<String, String> result = new HashMap<>();
+
+        json.get("results").forEach(entry -> {
+            result.put(entry.get("pubkey").asText(), entry.get("uid").asText());
+        });
+        return result;
+    }
+
+    public Map<String, String> getMembersUids(Peer peer) {
+        // get /wot/members
+        JsonNode json = executeRequest(peer, URL_MEMBERS, JsonNode.class);
+
+        if (json == null || !json.has("results")) return null;
+
+        Map<String, String> result = new HashMap<>();
+
+        json.get("results").forEach(entry -> {
+            result.put(entry.get("pubkey").asText(), entry.get("uid").asText());
+        });
+        return result;
+    }
+
+
     public void getRequirments(long currencyId, String pubKey) {
         if (log.isDebugEnabled()) {
             log.debug(String.format("Try to find user requirements on [%s]", pubKey));
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java
index b2ab891f..47c81eef 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java
@@ -58,7 +58,7 @@ public class CurrencyRegistryRemoteServiceImpl extends BaseRemoteServiceImpl imp
     public void afterPropertiesSet() {
         super.afterPropertiesSet();
         config = Configuration.instance();
-        peer = new Peer(config.getNodeElasticSearchHost(), config.getNodeElasticSearchPort());
+        peer = Peer.newBuilder().setHost(config.getNodeElasticSearchHost()).setPort(config.getNodeElasticSearchPort()).build();
     }
 
     @Override
@@ -97,7 +97,7 @@ public class CurrencyRegistryRemoteServiceImpl extends BaseRemoteServiceImpl imp
     @Override
     public List<String> getAllCurrencyNames() {
         if (log.isDebugEnabled()) {
-            log.debug("Getting all currency names...");
+            log.debug("Getting allOfToList currency names...");
         }
 
         // get currency
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyService.java
index 3dbdd246..f9cbe365 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyService.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyService.java
@@ -67,7 +67,7 @@ public interface CurrencyService extends Service {
     int getCurrencyCount();
 
     /**
-     * Fill all cache need for currencies
+     * Fill allOfToList cache need for currencies
      * @param context
      */
     void loadCache(long accountId);
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyServiceImpl.java
index 7ffc39c3..91b9be1f 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyServiceImpl.java
@@ -167,7 +167,7 @@ public class CurrencyServiceImpl implements CurrencyService, InitializingBean {
 
 
     /**
-     * Fill all cache need for currencies
+     * Fill allOfToList cache need for currencies
      * @param accountId
      */
     public void loadCache(long accountId) {
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java
new file mode 100644
index 00000000..501b5a87
--- /dev/null
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java
@@ -0,0 +1,57 @@
+package org.duniter.core.client.service.local;
+
+import org.duniter.core.beans.Service;
+import org.duniter.core.client.model.local.Peer;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Predicate;
+
+/**
+ * Created by blavenie on 20/03/17.
+ */
+public interface NetworkService extends Service {
+
+    class Sort {
+        public SortType sortType;
+        public boolean sortAsc;
+    }
+
+    class Filter {
+        public FilterType filterType;
+        public Peer.PeerStatus filterStatus;
+        public Boolean filterSsl;
+        public List<String> filterEndpoints;
+    }
+
+
+    enum SortType {
+        UID,
+        PUBKEY,
+        API,
+        HARDSHIP,
+        BLOCK_NUMBER
+    }
+
+    enum FilterType {
+        MEMBER, // Only members peers
+        MIRROR // Only mirror peers
+    }
+
+    List<Peer> getPeers(Peer mainPeer);
+
+    List<Peer> getPeers(Peer mainPeer, Filter filter, Sort sort);
+
+    CompletableFuture<List<CompletableFuture<Peer>>> asyncGetPeers(Peer mainPeer, ExecutorService pool) throws ExecutionException, InterruptedException;
+
+    List<Peer> fillPeerStatsConsensus(final List<Peer> peers);
+
+    Predicate<Peer> peerFilter(Filter filter);
+
+    Comparator<Peer> peerComparator(Sort sort);
+
+
+}
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java
new file mode 100644
index 00000000..6d0d6679
--- /dev/null
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java
@@ -0,0 +1,444 @@
+package org.duniter.core.client.service.local;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.ImmutableList;
+import org.duniter.core.client.model.bma.Constants;
+import org.duniter.core.client.model.bma.EndpointApi;
+import org.duniter.core.client.model.bma.NetworkPeering;
+import org.duniter.core.client.model.bma.NetworkPeers;
+import org.duniter.core.client.model.local.Peer;
+import org.duniter.core.client.service.ServiceLocator;
+import org.duniter.core.client.service.bma.BaseRemoteServiceImpl;
+import org.duniter.core.client.service.bma.NetworkRemoteService;
+import org.duniter.core.client.service.bma.WotRemoteService;
+import org.duniter.core.client.service.exception.HttpConnectException;
+import org.duniter.core.client.service.exception.HttpNotFoundException;
+import org.duniter.core.exception.TechnicalException;
+import org.duniter.core.service.CryptoService;
+import org.duniter.core.util.CollectionUtils;
+import org.duniter.core.util.Preconditions;
+import org.duniter.core.util.StringUtils;
+import org.duniter.core.util.concurrent.CompletableFutures;
+import org.duniter.core.util.websocket.WebsocketClientEndpoint;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Created by blavenie on 20/03/17.
+ */
+public class NetworkServiceImpl extends BaseRemoteServiceImpl implements NetworkService {
+
+    private static final Logger log = LoggerFactory.getLogger(NetworkServiceImpl.class);
+
+    private final static String BMA_URL_STATUS = "/node/summary";
+    private final static String BMA_URL_BLOCKCHAIN_CURRENT = "/blockchain/current";
+    private final static String BMA_URL_BLOCKCHAIN_HARDSHIP = "/blockchain/hardship/";
+
+    private NetworkRemoteService networkRemoteService;
+    private CryptoService cryptoService;
+    private WotRemoteService wotRemoteService;
+
+    public NetworkServiceImpl() {
+    }
+
+    public NetworkServiceImpl(NetworkRemoteService networkRemoteService,
+                              WotRemoteService wotRemoteService,
+                              CryptoService cryptoService) {
+        this();
+        this.networkRemoteService = networkRemoteService;
+        this.wotRemoteService = wotRemoteService;
+        this.cryptoService = cryptoService;
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+        super.afterPropertiesSet();
+        this.networkRemoteService = ServiceLocator.instance().getNetworkRemoteService();
+        this.wotRemoteService = ServiceLocator.instance().getWotRemoteService();
+        this.cryptoService = ServiceLocator.instance().getCryptoService();
+    }
+
+    @Override
+    public List<Peer> getPeers(Peer firstPeer) {
+
+        // Default filter
+        Filter filterDef = new Filter();
+        filterDef.filterType = null;
+        filterDef.filterStatus = Peer.PeerStatus.UP;
+        filterDef.filterEndpoints = ImmutableList.of(EndpointApi.BASIC_MERKLED_API.name(), EndpointApi.BMAS.name());
+
+        // Default sort
+        Sort sortDef = new Sort();
+        sortDef.sortType = null;
+
+        return getPeers(firstPeer, filterDef, sortDef);
+    }
+
+    @Override
+    public List<Peer> getPeers(Peer firstPeer, Filter filter, Sort sort) {
+
+        try {
+            return asyncGetPeers(firstPeer, null)
+                .thenCompose(CompletableFutures::allOfToList)
+                .thenApply(this::fillPeerStatsConsensus)
+                .thenApply(peers -> peers.stream()
+                        // filter, then sort
+                        .filter(peerFilter(filter))
+                        .sorted(peerComparator(sort))
+                        .collect(Collectors.toList()))
+                .thenApply(this::logPeers)
+                .get();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new TechnicalException("Error while loading peers: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public Predicate<Peer> peerFilter(final Filter filter) {
+        return peer -> applyPeerFilter(peer, filter);
+    }
+
+    @Override
+    public Comparator<Peer> peerComparator(final Sort sort) {
+        return Comparator.comparing(peer -> computePeerStatsScore(peer, sort), (score1, score2) -> score2.compareTo(score1));
+    }
+
+    @Override
+    public CompletableFuture<List<CompletableFuture<Peer>>> asyncGetPeers(Peer mainPeer, ExecutorService executor) throws ExecutionException, InterruptedException {
+        Preconditions.checkNotNull(mainPeer);
+
+        log.debug("Loading network peers...");
+
+        final ExecutorService pool = (executor != null) ? executor : ForkJoinPool.commonPool();
+
+        CompletableFuture<List<Peer>> peersFuture = CompletableFuture.supplyAsync(() -> loadPeerLeafs(mainPeer), pool);
+        CompletableFuture<Map<String, String>> memberUidsFuture = CompletableFuture.supplyAsync(() -> wotRemoteService.getMembersUids(mainPeer), pool);
+
+        return CompletableFuture.allOf(
+                new CompletableFuture[] {peersFuture, memberUidsFuture})
+                .thenApply(v -> {
+                    final Map<String, String> memberUids = memberUidsFuture.join();
+                    return peersFuture.join().stream().map(peer ->
+                        CompletableFuture.supplyAsync(() -> getVersion(peer), pool)
+                            .thenApply(this::getCurrentBlock)
+                            .exceptionally(throwable -> {
+                                peer.getStats().setStatus(Peer.PeerStatus.DOWN);
+                                if(!(throwable instanceof HttpConnectException)) {
+                                    Throwable cause = throwable.getCause() != null ? throwable.getCause() : throwable;
+                                    peer.getStats().setError(cause.getMessage());
+                                    if (log.isDebugEnabled()) {
+                                        if (log.isTraceEnabled()) {
+                                            log.debug(String.format("[%s] is DOWN: %s", peer, cause.getMessage()), cause);
+                                        }
+                                        else log.debug(String.format("[%s] is DOWN: %s", peer, cause.getMessage()));
+                                    }
+                                }
+                                else if (log.isTraceEnabled()) log.debug(String.format("[%s] is DOWN", peer));
+                                return peer;
+                            })
+                            .thenApply(apeer -> {
+                                String uid = StringUtils.isNotBlank(peer.getPubkey()) ? memberUids.get(peer.getPubkey()) : null;
+                                peer.getStats().setUid(uid);
+                                if (peer.getStats().isReacheable() && StringUtils.isNotBlank(uid)) {
+                                    getHardship(peer);
+                                }
+                                return apeer;
+                            })
+                            .exceptionally(throwable -> {
+                                peer.getStats().setHardshipLevel(0);
+                                return peer;
+                            })
+                        ).collect(Collectors.toList());
+                });
+    }
+
+    public List<Peer> fillPeerStatsConsensus(final List<Peer> peers) {
+
+        final Map<String,Long> peerCountByBuid = peers.stream()
+                .filter(peer -> peer.getStats().getStatus() == Peer.PeerStatus.UP)
+                .map(this::buid)
+                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
+
+        // Compute main consensus buid
+        Optional<Map.Entry<String, Long>> maxPeerCountEntry = peerCountByBuid.entrySet().stream()
+                .sorted(Comparator.comparing(Map.Entry::getValue, (l1, l2) -> l2.compareTo(l1)))
+                .findFirst();
+
+        final String mainBuid = maxPeerCountEntry.isPresent() ? maxPeerCountEntry.get().getKey() : null;;
+
+        // Compute total of UP peers
+        final Long peersUpTotal = peerCountByBuid.values().stream().mapToLong(Long::longValue).sum();
+
+        // Compute pct by buid
+        final Map<String, Double> buidsPct = peerCountByBuid.keySet().stream()
+                .collect(Collectors.toMap(
+                        buid -> buid,
+                        buid -> (peerCountByBuid.get(buid).doubleValue() * 100 / peersUpTotal)));
+
+        // Set consensus stats
+        peers.forEach(peer -> {
+                    Peer.Stats stats = peer.getStats();
+                    String buid = buid(stats);
+
+                    // Set consensus stats on each peers
+                    if (buid != null) {
+                        boolean isMainConsensus = buid.equals(mainBuid);
+                        stats.setMainConsensus(isMainConsensus);
+
+                        boolean isForkConsensus = !isMainConsensus && peerCountByBuid.get(buid) > 1;
+                        stats.setForkConsensus(isForkConsensus);
+
+                        stats.setConsensusPct(isMainConsensus || isForkConsensus ? buidsPct.get(buid) : 0d);
+                    }
+                });
+
+        return peers;
+    }
+
+    /* -- protected methods -- */
+
+    protected List<Peer> loadPeerLeafs(Peer peer) {
+        List<String> leaves = networkRemoteService.getPeersLeaves(peer);
+
+        if (CollectionUtils.isEmpty(leaves)) return new ArrayList<>(); // should never occur
+
+        // If less than 100 node, get it in ONE call
+        if (leaves.size() < 100) {
+            // TODO uncomment on prod
+            //List<Peer> peers = networkService.getPeers(peer);
+            //return ImmutableList.of(peers.get(0), peers.get(1), peers.get(2), peers.get(3));
+
+            //return networkService.getPeers(peer);
+        }
+
+        // Get it by multiple call on /network/peering?leaf=
+        List<Peer> result = new ArrayList<>();
+        if (CollectionUtils.isNotEmpty(leaves)) {
+            int offset = 0;
+            int count = Constants.Config.MAX_SAME_REQUEST_COUNT;
+            while (offset < leaves.size()) {
+                if (offset + count > leaves.size()) count = leaves.size() - offset;
+                loadPeerLeafs(peer, result, leaves, offset, count);
+                offset += count;
+                try {
+                    Thread.sleep(1000); // wait 1 s
+                } catch (InterruptedException e) {
+                    // stop
+                    offset = leaves.size();
+                }
+            }
+        }
+
+        return result;
+    }
+
+    protected void loadPeerLeafs(Peer requestedPeer, List<Peer> result, List<String> leaves, int offset, int count) {
+
+        for (int i = offset; i< offset + count; i++) {
+            String leaf = leaves.get(i);
+            try {
+                NetworkPeers.Peer peer = networkRemoteService.getPeerLeaf(requestedPeer, leaf);
+
+                if (CollectionUtils.isNotEmpty(peer.getEndpoints())) {
+                    for (NetworkPeering.Endpoint ep: peer.getEndpoints()) {
+                        if (ep != null && ep.getApi() != null) {
+                            Peer peerEp = Peer.newBuilder()
+                                    .setCurrency(peer.getCurrency())
+                                    .setHash(leaf)
+                                    .setPubkey(peer.getPubkey())
+                                    .setEndpoint(ep)
+                                    .build();
+                            result.add(peerEp);
+                        }
+                    }
+                }
+
+
+            } catch(HttpNotFoundException e) {
+                log.warn("Peer not found for leaf=" + leaf);
+                // skip
+            }
+        }
+    }
+
+
+    protected boolean applyPeerFilter(Peer peer, Filter filter) {
+
+        Peer.Stats stats = peer.getStats();
+
+        // Filter member or mirror
+        if (filter.filterType != null && (
+                (filter.filterType == FilterType.MEMBER && StringUtils.isBlank(stats.getUid()))
+                        || (filter.filterType == FilterType.MIRROR && StringUtils.isNotBlank(stats.getUid()))
+        )) {
+            return false;
+        }
+
+        // Filter on endpoints
+        if (CollectionUtils.isNotEmpty(filter.filterEndpoints)
+                && (StringUtils.isBlank(peer.getApi())
+                    || !filter.filterEndpoints.contains(peer.getApi()))) {
+            return false;
+        }
+
+        // Filter on status
+        if (filter.filterStatus != null && filter.filterStatus != stats.getStatus()) {
+            return false;
+        }
+
+        // Filter on SSL
+        if (filter.filterSsl != null && filter.filterSsl.booleanValue() != peer.isUseSsl()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    protected Peer getVersion(final Peer peer) {
+        JsonNode json = executeRequest(peer, BMA_URL_STATUS, JsonNode.class);
+        // TODO update peer
+        json = json.get("duniter");
+        if (json.isMissingNode()) throw new TechnicalException(String.format("Invalid format of [%s] response", BMA_URL_STATUS));
+        json = json.get("version");
+        if (json.isMissingNode()) throw new TechnicalException(String.format("No version attribute found in [%s] response", BMA_URL_STATUS));
+        String version = json.asText();
+        peer.getStats().setVersion(version);
+        return peer;
+    }
+
+    protected Peer getCurrentBlock(final Peer peer) {
+        JsonNode json = executeRequest(peer, BMA_URL_BLOCKCHAIN_CURRENT , JsonNode.class);
+
+        Integer number = json.has("number") ? json.get("number").asInt() : null;
+        peer.getStats().setBlockNumber(number);
+
+        String hash = json.has("hash") ? json.get("hash").asText() : null;
+        peer.getStats().setBlockHash(hash);
+
+        Long medianTime = json.has("medianTime") ? json.get("medianTime").asLong() : null;
+        peer.getStats().setMedianTime(medianTime);
+
+        if (log.isTraceEnabled()) {
+            log.trace(String.format("[%s] current block [%s-%s]", peer.toString(), number, hash));
+        }
+
+        return peer;
+    }
+
+    protected Peer getHardship(final Peer peer) {
+        if (StringUtils.isBlank(peer.getPubkey())) return peer;
+
+        JsonNode json = executeRequest(peer, BMA_URL_BLOCKCHAIN_HARDSHIP + peer.getPubkey(), JsonNode.class);
+        Integer level = json.has("level") ? json.get("level").asInt() : null;
+        peer.getStats().setHardshipLevel(level);
+        return peer;
+    }
+
+    protected String computeUniqueId(Peer peer) {
+        return cryptoService.hash(
+                new StringJoiner("|")
+                .add(peer.getPubkey())
+                .add(peer.getDns())
+                .add(peer.getIpv4())
+                .add(peer.getIpv6())
+                .add(String.valueOf(peer.getPort()))
+                .add(Boolean.toString(peer.isUseSsl()))
+                .toString());
+    }
+
+    protected JsonNode get(final Peer peer, String path) {
+        return executeRequest(peer, path, JsonNode.class);
+    }
+
+    /**
+     * Log allOfToList peers found
+     */
+    protected List<Peer> logPeers(final List<Peer> peers) {
+        if (!log.isDebugEnabled()) return peers;
+
+        if (CollectionUtils.isEmpty(peers)) {
+            log.debug("No peers found.");
+        }
+        else {
+            log.debug(String.format("Found %s peers", peers.size()));
+            if (log.isTraceEnabled()) {
+
+                peers.forEach(peerFound -> {
+                    if (peerFound.getStats().getStatus() == Peer.PeerStatus.DOWN) {
+                        String error = peerFound.getStats().getError();
+                        log.trace(String.format("Found peer [%s] [%s] %s",
+                                peerFound.toString(),
+                                peerFound.getStats().getStatus().name(),
+                                error != null ? error : ""));
+                    } else {
+                        log.trace(String.format("Found peer [%s] [%s] [v%s] block [%s]", peerFound.toString(),
+                                peerFound.getStats().getStatus().name(),
+                                peerFound.getStats().getVersion(),
+                                peerFound.getStats().getBlockNumber()
+                        ));
+
+                    }
+                });
+            }
+        }
+        return peers;
+    }
+
+    protected double computePeerStatsScore(Peer peer, Sort sort) {
+        double score = 0;
+        Peer.Stats stats = peer.getStats();
+        if (sort != null && sort.sortType != null) {
+            long specScore = 0;
+            specScore += (sort.sortType == SortType.UID ? computeScoreAlphaValue(stats.getUid(), 3, sort.sortAsc) : 0);
+            specScore += (sort.sortType == SortType.PUBKEY ? computeScoreAlphaValue(peer.getPubkey(), 3, sort.sortAsc) : 0);
+            specScore += (sort.sortType == SortType.API ?
+                    (peer.isUseSsl() ? (sort.sortAsc ? 1 : -1) :
+                            (hasEndPointAPI(peer, EndpointApi.ES_USER_API) ? (sort.sortAsc ? 0.5 : -0.5) : 0)) : 0);
+            specScore += (sort.sortType == SortType.HARDSHIP ? (stats.getHardshipLevel() != null ? (sort.sortAsc ? (10000-stats.getHardshipLevel()) : stats.getHardshipLevel()): 0) : 0);
+            specScore += (sort.sortType == SortType.BLOCK_NUMBER ? (stats.getBlockNumber() != null ? (sort.sortAsc ? (1000000000 - stats.getBlockNumber()) : stats.getBlockNumber()) : 0) : 0);
+            score += (10000000000l * specScore);
+        }
+        score += (1000000000 * (stats.getStatus() == Peer.PeerStatus.UP ? 1 : 0));
+        score += (100000000  * (stats.isMainConsensus() ? 1 : 0));
+        score += (1000000    * (stats.isForkConsensus() ? stats.getConsensusPct() : 0));
+
+        score += (100     * (stats.getHardshipLevel() != null ? (10000-stats.getHardshipLevel()) : 0));
+        score += /* 1     */(peer.getPubkey() != null ? computeScoreAlphaValue(peer.getPubkey(), 2, true) : 0);
+
+        return score;
+    }
+
+    protected int computeScoreAlphaValue(String value, int nbChars, boolean asc) {
+        if (StringUtils.isBlank(value)) return 0;
+        int score = 0;
+        value = value.toLowerCase();
+        if (nbChars > value.length()) {
+            nbChars = value.length();
+        }
+        score += (int)value.charAt(0);
+        for (int i=1; i < nbChars; i++) {
+            score += Math.pow(0.001, i) * value.charAt(i);
+        }
+        return asc ? (1000 - score) : score;
+    }
+
+    protected boolean hasEndPointAPI(Peer peer, EndpointApi api) {
+        return peer.getApi() != null && peer.getApi().equalsIgnoreCase(api.name());
+    }
+
+    protected String buid(Peer peer) {
+        return buid(peer.getStats());
+    }
+
+    protected String buid(Peer.Stats stats) {
+        return stats.getStatus() == Peer.PeerStatus.UP
+                ? stats.getBlockNumber() + "-" + stats.getBlockHash()
+                : null;
+    }
+}
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/PeerServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/PeerServiceImpl.java
index d8f36924..f724f157 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/PeerServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/PeerServiceImpl.java
@@ -150,7 +150,7 @@ public class PeerServiceImpl implements PeerService, InitializingBean {
     }
 
     /**
-     * Fill all cache need for currencies
+     * Fill allOfToList cache need for currencies
      * @param accountId
      */
     public void loadCache(long accountId) {
diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/TestResource.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/TestResource.java
index 28d37dbd..1029dadb 100644
--- a/duniter4j-core-client/src/test/java/org/duniter/core/client/TestResource.java
+++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/TestResource.java
@@ -155,7 +155,10 @@ public class TestResource extends org.duniter.core.test.TestResource {
         // Set a default account id, then load cache
         ServiceLocator.instance().getDataContext().setAccountId(0);
 
-        Peer peer = new Peer(config.getNodeHost(), config.getNodePort());
+        Peer peer = Peer.newBuilder()
+                .setHost(config.getNodeHost())
+                .setPort(config.getNodePort())
+                .build();
         peer.setCurrencyId(fixtures.getDefaultCurrencyId());
 
         ServiceLocator.instance().getPeerService().save(peer);
diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java
index 0f96bc8b..62eb7714 100644
--- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java
+++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java
@@ -25,19 +25,13 @@ package org.duniter.core.client.service;
 
 import org.duniter.core.client.TestResource;
 import org.duniter.core.client.config.Configuration;
-import org.duniter.core.client.model.bma.EndpointProtocol;
-import org.duniter.core.client.model.bma.NetworkPeering;
 import org.duniter.core.client.model.local.Peer;
-import org.duniter.core.client.service.bma.NetworkRemoteService;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.List;
-
 public class HttpServiceTest {
 
 	private static final Logger log = LoggerFactory.getLogger(HttpServiceTest.class);
@@ -64,10 +58,9 @@ public class HttpServiceTest {
 	/* -- internal methods */
 
     protected Peer createTestPeer() {
-        Peer peer = new Peer(
-                Configuration.instance().getNodeHost(),
-                Configuration.instance().getNodePort());
-
-        return peer;
+		return Peer.newBuilder()
+				.setHost(Configuration.instance().getNodeHost())
+				.setPort(Configuration.instance().getNodePort())
+				.build();
     }
 }
diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceTest.java
index fb881047..33ee0035 100644
--- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceTest.java
+++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceTest.java
@@ -114,7 +114,7 @@ public class BlockchainRemoteServiceTest {
         Assert.assertNotNull(result);
         Assert.assertEquals(10, result.length);
 
-        // Make sure all json are valid blocks
+        // Make sure allOfToList json are valid blocks
         ObjectMapper objectMapper = JacksonUtils.newObjectMapper();
 
         int number = 0;
@@ -192,11 +192,10 @@ public class BlockchainRemoteServiceTest {
     /* -- Internal methods -- */
 
     protected Peer createTestPeer() {
-        Peer peer = new Peer(
-                Configuration.instance().getNodeHost(),
-                Configuration.instance().getNodePort());
-
-        return peer;
+        return Peer.newBuilder()
+                .setHost(Configuration.instance().getNodeHost())
+                .setPort(Configuration.instance().getNodePort())
+                .build();
     }
 
     protected Wallet createTestWallet() {
diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java
index 5544aee2..ccd274e8 100644
--- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java
+++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java
@@ -25,7 +25,7 @@ package org.duniter.core.client.service.bma;
 
 import org.duniter.core.client.TestResource;
 import org.duniter.core.client.config.Configuration;
-import org.duniter.core.client.model.bma.EndpointProtocol;
+import org.duniter.core.client.model.bma.EndpointApi;
 import org.duniter.core.client.model.bma.NetworkPeering;
 import org.duniter.core.client.model.local.Peer;
 import org.duniter.core.client.service.ServiceLocator;
@@ -69,7 +69,7 @@ public class NetworkRemoteServiceTest {
 	@Test
 	public void findPeers() throws Exception {
 
-		List<Peer> result = service.findPeers(peer, null, EndpointProtocol.BASIC_MERKLED_API, null, null);
+		List<Peer> result = service.findPeers(peer, null, EndpointApi.BASIC_MERKLED_API, null, null);
 
 		Assert.assertNotNull(result);
 		Assert.assertTrue(result.size() > 0);
@@ -87,10 +87,9 @@ public class NetworkRemoteServiceTest {
 	/* -- internal methods */
 
     protected Peer createTestPeer() {
-        Peer peer = new Peer(
-                Configuration.instance().getNodeHost(),
-                Configuration.instance().getNodePort());
-
-        return peer;
+		return Peer.newBuilder()
+				.setHost(Configuration.instance().getNodeHost())
+				.setPort(Configuration.instance().getNodePort())
+				.build();
     }
 }
diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/TransactionRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/TransactionRemoteServiceTest.java
index 8d8950c6..3118e7f3 100644
--- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/TransactionRemoteServiceTest.java
+++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/TransactionRemoteServiceTest.java
@@ -90,10 +90,9 @@ public class TransactionRemoteServiceTest {
 	}
 
     protected Peer createTestPeer() {
-        Peer peer = new Peer(
-                Configuration.instance().getNodeHost(),
-                Configuration.instance().getNodePort());
-
-        return peer;
+		return Peer.newBuilder()
+				.setHost(Configuration.instance().getNodeHost())
+				.setPort(Configuration.instance().getNodePort())
+				.build();
     }
 }
diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/WotRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/WotRemoteServiceTest.java
index 5cbbd36e..ca52ee15 100644
--- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/WotRemoteServiceTest.java
+++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/WotRemoteServiceTest.java
@@ -165,10 +165,9 @@ public class WotRemoteServiceTest {
 	}
 
 	protected Peer createTestPeer() {
-		Peer peer = new Peer(
-				Configuration.instance().getNodeHost(),
-				Configuration.instance().getNodePort());
-
-		return peer;
+		return Peer.newBuilder()
+				.setHost(Configuration.instance().getNodeHost())
+				.setPort(Configuration.instance().getNodePort())
+				.build();
 	}
 }
diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/local/NetworkServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/local/NetworkServiceTest.java
new file mode 100644
index 00000000..8106d95f
--- /dev/null
+++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/local/NetworkServiceTest.java
@@ -0,0 +1,72 @@
+package org.duniter.core.client.service.local;
+
+/*
+ * #%L
+ * UCoin Java Client :: Core API
+ * %%
+ * Copyright (C) 2014 - 2015 EIS
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the 
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public 
+ * License along with this program.  If not, see
+ * <http://www.gnu.org/licenses/gpl-3.0.html>.
+ * #L%
+ */
+
+
+import org.duniter.core.client.TestResource;
+import org.duniter.core.client.config.Configuration;
+import org.duniter.core.client.model.local.Peer;
+import org.duniter.core.client.service.ServiceLocator;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class NetworkServiceTest {
+
+	private static final Logger log = LoggerFactory.getLogger(NetworkServiceTest.class);
+
+	@ClassRule
+	public static final TestResource resource = TestResource.create();
+	
+	private NetworkService service;
+	private Peer peer;
+	
+	@Before
+	public void setUp() {
+		peer = createTestPeer();
+		service = ServiceLocator.instance().getNetworkService();
+	}
+
+	@Test
+	public void start() throws Exception {
+
+		List<Peer> peers = service.getPeers(peer);
+
+		Assert.assertNotNull(peers);
+		Assert.assertTrue(peers.size() > 0);
+	}
+
+	/* -- internal methods */
+
+    protected Peer createTestPeer() {
+        return Peer.newBuilder()
+				.setHost(Configuration.instance().getNodeHost())
+				.setPort(Configuration.instance().getNodePort())
+				.build();
+    }
+}
diff --git a/duniter4j-core-client/src/test/resources/META-INF/services/org.duniter.core.beans.Bean b/duniter4j-core-client/src/test/resources/META-INF/services/org.duniter.core.beans.Bean
index e4fdb2bb..bbf9fb6b 100644
--- a/duniter4j-core-client/src/test/resources/META-INF/services/org.duniter.core.beans.Bean
+++ b/duniter4j-core-client/src/test/resources/META-INF/services/org.duniter.core.beans.Bean
@@ -8,5 +8,6 @@ org.duniter.core.client.service.HttpServiceImpl
 org.duniter.core.client.service.DataContext
 org.duniter.core.client.service.local.PeerServiceImpl
 org.duniter.core.client.service.local.CurrencyServiceImpl
+org.duniter.core.client.service.local.NetworkServiceImpl
 org.duniter.core.client.dao.mem.MemoryCurrencyDaoImpl
 org.duniter.core.client.dao.mem.MemoryPeerDaoImpl
\ No newline at end of file
diff --git a/duniter4j-core-client/src/test/resources/duniter4j-core-client-test.properties b/duniter4j-core-client/src/test/resources/duniter4j-core-client-test.properties
index 9e017425..d9f4f75b 100644
--- a/duniter4j-core-client/src/test/resources/duniter4j-core-client-test.properties
+++ b/duniter4j-core-client/src/test/resources/duniter4j-core-client-test.properties
@@ -1,5 +1,5 @@
-duniter4j.node.host=gtest.duniter.org
-duniter4j.node.port=10900
+duniter4j.node.host=192.168.0.5
+duniter4j.node.port=10901
 
 duniter4j.node.elasticsearch.host=localhost
 duniter4j.node.elasticsearch.port=9200
diff --git a/duniter4j-core-client/src/test/resources/log4j.properties b/duniter4j-core-client/src/test/resources/log4j.properties
index a3e37387..e1c60841 100644
--- a/duniter4j-core-client/src/test/resources/log4j.properties
+++ b/duniter4j-core-client/src/test/resources/log4j.properties
@@ -7,12 +7,7 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender
 log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
 log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %5p (%c:%L) - %m%n
 
-# ucoin levels
-log4j.logger.org.duniter=INFO
-log4j.logger.org.duniter.core.client.service=DEBUG
-log4j.logger.org.duniter.core.client.service.bma=DEBUG
-log4j.logger.org.duniter.core.beans=WARN
-
+# File output
 log4j.appender.file=org.apache.log4j.RollingFileAppender
 log4j.appender.file.file=ucoin-client.log
 log4j.appender.file.MaxFileSize=10MB
@@ -21,4 +16,15 @@ log4j.appender.file.MaxBackupIndex=4
 log4j.appender.file.layout=org.apache.log4j.PatternLayout
 log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %5p %c - %m%n
 
+# duniter4j levels
+log4j.logger.org.duniter=INFO
+log4j.logger.org.duniter.core.client.service=DEBUG
+log4j.logger.org.duniter.core.client.service.bma=DEBUG
+log4j.logger.org.duniter.core.beans=WARN
+log4j.logger.org.duniter.core.client.service=TRACE
+
+# Framework levels
+log4j.logger.org.apache.http=WARN
+
+
 
diff --git a/duniter4j-core-shared/pom.xml b/duniter4j-core-shared/pom.xml
index 7844bdea..f826f638 100644
--- a/duniter4j-core-shared/pom.xml
+++ b/duniter4j-core-shared/pom.xml
@@ -41,6 +41,16 @@
       <artifactId>scrypt</artifactId>
     </dependency>
 
+    <!-- http -->
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpcore</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>javax.websocket</groupId>
       <artifactId>javax.websocket-api</artifactId>
diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/beans/BeanFactory.java b/duniter4j-core-shared/src/main/java/org/duniter/core/beans/BeanFactory.java
index 4d1b4d45..4567cea4 100644
--- a/duniter4j-core-shared/src/main/java/org/duniter/core/beans/BeanFactory.java
+++ b/duniter4j-core-shared/src/main/java/org/duniter/core/beans/BeanFactory.java
@@ -120,7 +120,6 @@ public class BeanFactory implements Closeable{
         throw new TechnicalException(String.format("Unable to create bean with type [%s]: not configured for the service loader [%s]", clazz.getName(), Bean.class.getCanonicalName()));
     }
 
-
     @Override
     public void close() throws IOException {
         for(Object bean: beansCache.values()) {
diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/util/concurrent/CompletableFutures.java b/duniter4j-core-shared/src/main/java/org/duniter/core/util/concurrent/CompletableFutures.java
new file mode 100644
index 00000000..b0b78f91
--- /dev/null
+++ b/duniter4j-core-shared/src/main/java/org/duniter/core/util/concurrent/CompletableFutures.java
@@ -0,0 +1,38 @@
+package org.duniter.core.util.concurrent;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class on CompletableFuture and concurrent classes
+ * Created by blavenie on 24/03/17.
+ */
+public class CompletableFutures {
+
+    private CompletableFutures() {
+    }
+
+    public static <T> CompletableFuture<List<T>> allOfToList(List<CompletableFuture<T>> futures) {
+        CompletableFuture<Void> allDoneFuture =
+                CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]));
+        return allDoneFuture.thenApply(v ->
+                futures.stream()
+                        .map(future -> future.join())
+                        .filter(peer -> peer != null) // skip
+                        .collect(Collectors.toList())
+        );
+    }
+
+    public static <T> CompletableFuture<List<T>> allOfToList(List<CompletableFuture<T>> futures, Predicate<? super T> filter) {
+        CompletableFuture<Void> allDoneFuture =
+                CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]));
+        return allDoneFuture.thenApply(v ->
+                futures.stream()
+                        .map(future -> future.join())
+                        .filter(filter)
+                        .collect(Collectors.toList())
+        );
+    }
+}
diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/util/http/InetAddressUtils.java b/duniter4j-core-shared/src/main/java/org/duniter/core/util/http/InetAddressUtils.java
new file mode 100644
index 00000000..b88050ef
--- /dev/null
+++ b/duniter4j-core-shared/src/main/java/org/duniter/core/util/http/InetAddressUtils.java
@@ -0,0 +1,27 @@
+package org.duniter.core.util.http;
+
+import java.util.regex.Pattern;
+
+/**
+ * Created by blavenie on 24/03/17.
+ */
+public class InetAddressUtils {
+
+    public static final Pattern LOCAL_IP_ADDRESS_PATTERN = Pattern.compile("^127[.]0[.]0.|192[.]168[.]|10[.]0[.]0[.]|172[.]16[.]");
+
+    private InetAddressUtils() {
+    }
+
+    public static boolean isNotLocalIPv4Address(String input) {
+        return org.apache.http.conn.util.InetAddressUtils.isIPv4Address(input) &&
+                !LOCAL_IP_ADDRESS_PATTERN.matcher(input).matches();
+    }
+
+    public static boolean isIPv4Address(String input) {
+        return org.apache.http.conn.util.InetAddressUtils.isIPv4Address(input);
+    }
+
+    public static boolean isIPv6Address(String input) {
+        return org.apache.http.conn.util.InetAddressUtils.isIPv6Address(input);
+    }
+}
diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/util/websocket/WebsocketClientEndpoint.java b/duniter4j-core-shared/src/main/java/org/duniter/core/util/websocket/WebsocketClientEndpoint.java
index 6a58a068..2f127d8d 100644
--- a/duniter4j-core-shared/src/main/java/org/duniter/core/util/websocket/WebsocketClientEndpoint.java
+++ b/duniter4j-core-shared/src/main/java/org/duniter/core/util/websocket/WebsocketClientEndpoint.java
@@ -136,6 +136,17 @@ public class WebsocketClientEndpoint implements Closeable {
         }
     }
 
+    /**
+     * unregister message listener
+     *
+     * @param listener
+     */
+    public void unregisterListener(MessageListener listener) {
+        synchronized (messageListeners) {
+            this.messageListeners.remove(listener);
+        }
+    }
+
     /**
      * register connection listener
      *
@@ -147,6 +158,18 @@ public class WebsocketClientEndpoint implements Closeable {
         }
     }
 
+    /**
+     * unregister connection listener
+     *
+     * @param listener
+     */
+    public void unregisterListener(ConnectionListener listener) {
+        synchronized (connectionListeners) {
+            this.connectionListeners.remove(listener);
+        }
+    }
+
+
     /**
      * Send a message.
      *
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginInit.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginInit.java
index b8d4bde5..89d27739 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginInit.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginInit.java
@@ -27,6 +27,7 @@ import org.duniter.core.client.model.local.Peer;
 import org.duniter.elasticsearch.rest.security.RestSecurityController;
 import org.duniter.elasticsearch.service.BlockchainService;
 import org.duniter.elasticsearch.service.CurrencyService;
+import org.duniter.elasticsearch.service.NetworkService;
 import org.duniter.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
@@ -135,6 +136,11 @@ public class PluginInit extends AbstractLifecycleComponent<PluginInit> {
             injector.getInstance(BlockchainService.class)
                     .indexLastBlocks(peer)
                     .listenAndIndexNewBlock(peer);
+
+            // Index peers (and listen if new peer appear)
+            injector.getInstance(NetworkService.class)
+                    .indexLastPeers(peer)/*
+                    .listenAndIndexNewPeer(peer)*/;
         }
     }
 }
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java
index 50cb48e4..e95270d3 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java
@@ -105,7 +105,6 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> {
         applicationConfig.setDefaultOption(ConfigurationOption.BASEDIR.getKey(), baseDir);
         applicationConfig.setDefaultOption(ConfigurationOption.NODE_HOST.getKey(), getNodeBmaHost());
         applicationConfig.setDefaultOption(ConfigurationOption.NODE_PORT.getKey(), String.valueOf(getNodeBmaPort()));
-        applicationConfig.setDefaultOption(ConfigurationOption.NODE_PROTOCOL.getKey(), getNodeBmaUseSsl() ? "https" : "http");
         applicationConfig.setDefaultOption(ConfigurationOption.NETWORK_TIMEOUT.getKey(), String.valueOf(getNetworkTimeout()));
 
         try {
@@ -168,7 +167,7 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> {
     }
 
     public boolean getNodeBmaUseSsl() {
-        return settings.getAsBoolean("duniter.useSsl", getNodeBmaPort() == 443);
+        return settings.getAsBoolean("duniter.useSsl", null);
     }
 
     public boolean isIndexBulkEnable() {
@@ -227,7 +226,7 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> {
             return null;
         }
 
-        Peer peer = new Peer(getNodeBmaHost(), getNodeBmaPort(), getNodeBmaUseSsl());
+        Peer peer = Peer.newBuilder().setHost(getNodeBmaHost()).setPort(getNodeBmaPort()).setUseSsl(getNodeBmaUseSsl()).build();
         return peer;
     }
 
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java
index ce82dfc3..8f3192de 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java
@@ -30,7 +30,7 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import org.duniter.core.client.model.bma.BlockchainBlock;
 import org.duniter.core.client.model.bma.BlockchainParameters;
-import org.duniter.core.client.model.bma.EndpointProtocol;
+import org.duniter.core.client.model.bma.EndpointApi;
 import org.duniter.core.client.model.local.Peer;
 import org.duniter.core.util.json.JsonAttributeParser;
 import org.duniter.core.client.model.bma.jackson.JacksonUtils;
@@ -178,7 +178,7 @@ public class BlockchainService extends AbstractService {
             // Check if index exists
             createIndexIfNotExists(currencyName, true/*wait cluster health*/);
 
-            // Then index all blocks
+            // Then index allOfToList blocks
             BlockchainBlock peerCurrentBlock = blockchainRemoteService.getCurrentBlock(peer);
 
             if (peerCurrentBlock != null) {
@@ -243,7 +243,7 @@ public class BlockchainService extends AbstractService {
                         progressionModel.setStatus(ProgressionModel.Status.SUCCESS);
                     }
                     else {
-                        logger.warn(String.format("[%s] [%s] Could not indexed all blocks. Missing %s blocks.", currencyName, peer, missingBlocks.size()));
+                        logger.warn(String.format("[%s] [%s] Could not indexed allOfToList blocks. Missing %s blocks.", currencyName, peer, missingBlocks.size()));
                         progressionModel.setStatus(ProgressionModel.Status.FAILED);
                     }
                 }
@@ -294,7 +294,7 @@ public class BlockchainService extends AbstractService {
                 .build();
         createIndexRequestBuilder.setSettings(indexSettings);
         createIndexRequestBuilder.addMapping(BLOCK_TYPE, createBlockTypeMapping());
-        createIndexRequestBuilder.addMapping(PEER_TYPE, createPeerTypeMapping());
+        createIndexRequestBuilder.addMapping(PEER_TYPE, NetworkService.createPeerTypeMapping());
         createIndexRequestBuilder.execute().actionGet();
     }
 
@@ -689,34 +689,6 @@ public class BlockchainService extends AbstractService {
         }
     }
 
-    public XContentBuilder createPeerTypeMapping() {
-        try {
-            XContentBuilder mapping = XContentFactory.jsonBuilder()
-                    .startObject()
-                    .startObject(PEER_TYPE)
-                    .startObject("properties")
-
-                    // currency
-                    .startObject("currency")
-                    .field("type", "string")
-                    .endObject()
-
-                    // pubkey
-                    .startObject("pubkey")
-                    .field("type", "string")
-                    .field("index", "not_analyzed")
-                    .endObject()
-
-
-                    .endObject()
-                    .endObject().endObject();
-
-            return mapping;
-        }
-        catch(IOException ioe) {
-            throw new TechnicalException("Error while getting mapping for block index: " + ioe.getMessage(), ioe);
-        }
-    }
 
     public BlockchainBlock getBlockByIdStr(String currencyName, String blockId) {
 
@@ -954,9 +926,9 @@ public class BlockchainService extends AbstractService {
         // Select other peers, in filtering on the same blockchain version
 
         // TODO : a activer quand les peers seront bien mis à jour (UP/DOWN, block, hash...)
-        //List<Peer> otherPeers = networkRemoteService.findPeers(peer, "UP", EndpointProtocol.BASIC_MERKLED_API,
+        //List<Peer> otherPeers = networkRemoteService.findPeers(peer, "UP", EndpointApi.BASIC_MERKLED_API,
         //        currentBlock.getNumber(), currentBlock.getHash());
-        List<Peer> otherPeers = networkRemoteService.findPeers(peer, null, EndpointProtocol.BASIC_MERKLED_API,
+        List<Peer> otherPeers = networkRemoteService.findPeers(peer, null, EndpointApi.BASIC_MERKLED_API,
                 null, null);
 
         for(Peer childPeer: otherPeers) {
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/NetworkService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/NetworkService.java
new file mode 100644
index 00000000..209abd26
--- /dev/null
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/NetworkService.java
@@ -0,0 +1,571 @@
+package org.duniter.elasticsearch.service;
+
+/*
+ * #%L
+ * UCoin Java Client :: Core API
+ * %%
+ * Copyright (C) 2014 - 2015 EIS
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the 
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public 
+ * License along with this program.  If not, see
+ * <http://www.gnu.org/licenses/gpl-3.0.html>.
+ * #L%
+ */
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.duniter.core.client.model.bma.BlockchainParameters;
+import org.duniter.core.client.model.bma.EndpointApi;
+import org.duniter.core.client.model.bma.jackson.JacksonUtils;
+import org.duniter.core.client.model.local.Peer;
+import org.duniter.core.client.service.local.NetworkServiceImpl;
+import org.duniter.core.exception.TechnicalException;
+import org.duniter.core.model.NullProgressionModel;
+import org.duniter.core.model.ProgressionModel;
+import org.duniter.core.util.CollectionUtils;
+import org.duniter.core.util.Preconditions;
+import org.duniter.core.util.StringUtils;
+import org.duniter.core.util.concurrent.CompletableFutures;
+import org.duniter.core.util.json.JsonAttributeParser;
+import org.duniter.core.util.json.JsonSyntaxException;
+import org.duniter.core.util.websocket.WebsocketClientEndpoint;
+import org.duniter.elasticsearch.PluginSettings;
+import org.duniter.elasticsearch.exception.DuplicateIndexIdException;
+import org.duniter.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.action.ActionFuture;
+import org.elasticsearch.action.index.IndexRequestBuilder;
+import org.elasticsearch.action.index.IndexResponse;
+import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.search.SearchType;
+import org.elasticsearch.action.update.UpdateRequestBuilder;
+import org.elasticsearch.action.update.UpdateResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.SearchHitField;
+import org.elasticsearch.search.highlight.HighlightField;
+import org.elasticsearch.search.sort.SortOrder;
+import org.nuiton.i18n.I18n;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+/**
+ * Created by Benoit on 30/03/2015.
+ */
+public class NetworkService extends AbstractService {
+
+    public static final String PEER_TYPE = "peer";
+
+    private final ProgressionModel nullProgressionModel = new NullProgressionModel();
+
+    private org.duniter.core.client.service.bma.BlockchainRemoteService blockchainRemoteService;
+    private org.duniter.core.client.service.local.NetworkService networkService;
+    private ThreadPool threadPool;
+    private List<WebsocketClientEndpoint.ConnectionListener> connectionListeners = new ArrayList<>();
+    private final WebsocketClientEndpoint.ConnectionListener dispatchConnectionListener;
+
+    private final JsonAttributeParser blockCurrencyParser = new JsonAttributeParser("currency");
+    private final JsonAttributeParser blockHashParser = new JsonAttributeParser("hash");
+
+    private ObjectMapper objectMapper;
+
+    @Inject
+    public NetworkService(Client client, PluginSettings settings, ThreadPool threadPool,
+                          final ServiceLocator serviceLocator){
+        super("duniter.network", client, settings);
+        this.objectMapper = JacksonUtils.newObjectMapper();
+        this.threadPool = threadPool;
+        threadPool.scheduleOnStarted(() -> {
+            this.blockchainRemoteService = serviceLocator.getBlockchainRemoteService();
+            this.networkService = serviceLocator.getNetworkService();
+        });
+        dispatchConnectionListener = new WebsocketClientEndpoint.ConnectionListener() {
+            @Override
+            public void onSuccess() {
+                synchronized (connectionListeners) {
+                    connectionListeners.stream().forEach(connectionListener -> connectionListener.onSuccess());
+                }
+            }
+            @Override
+            public void onError(Exception e, long lastTimeUp) {
+                synchronized (connectionListeners) {
+                    connectionListeners.stream().forEach(connectionListener -> connectionListener.onError(e, lastTimeUp));
+                }
+            }
+        };
+    }
+
+    public void registerConnectionListener(WebsocketClientEndpoint.ConnectionListener listener) {
+        synchronized (connectionListeners) {
+            connectionListeners.add(listener);
+        }
+    }
+
+    public NetworkService indexLastPeers(Peer peer) {
+        indexLastPeers(peer, nullProgressionModel);
+        return this;
+    }
+
+    public NetworkService indexLastPeers(Peer peer, ProgressionModel progressionModel) {
+
+        try {
+            // Get the blockchain name from node
+            BlockchainParameters parameter = blockchainRemoteService.getParameters(peer);
+            if (parameter == null) {
+                progressionModel.setStatus(ProgressionModel.Status.FAILED);
+                logger.error(I18n.t("duniter4j.es.networkService.indexPeers.remoteParametersError", peer));
+                return this;
+            }
+            String currencyName = parameter.getCurrency();
+
+            indexPeers(currencyName, peer, progressionModel);
+
+        } catch(Exception e) {
+            logger.error("Error during indexLastPeers: " + e.getMessage(), e);
+            progressionModel.setStatus(ProgressionModel.Status.FAILED);
+        }
+
+        return this;
+    }
+
+
+    public NetworkService indexPeers(String currencyName, Peer firstPeer, ProgressionModel progressionModel) {
+        progressionModel.setStatus(ProgressionModel.Status.RUNNING);
+        progressionModel.setTotal(100);
+        long timeStart = System.currentTimeMillis();
+
+        try {
+
+            progressionModel.setTask(I18n.t("duniter4j.es.networkService.indexPeers.task", currencyName, firstPeer));
+            logger.info(I18n.t("duniter4j.es.networkService.indexPeers.task", currencyName, firstPeer));
+
+            // Default filter
+            org.duniter.core.client.service.local.NetworkService.Filter filterDef = new org.duniter.core.client.service.local.NetworkService.Filter();
+            filterDef.filterType = null;
+            filterDef.filterStatus = Peer.PeerStatus.UP;
+            filterDef.filterEndpoints = ImmutableList.of(EndpointApi.BASIC_MERKLED_API.name(), EndpointApi.BMAS.name());
+
+            // Default sort
+            org.duniter.core.client.service.local.NetworkService.Sort sortDef = new org.duniter.core.client.service.local.NetworkService.Sort();
+            sortDef.sortType = null;
+
+            try {
+                networkService.asyncGetPeers(firstPeer, threadPool.scheduler())
+                        .thenCompose(CompletableFutures::allOfToList)
+                        .thenApply(networkService::fillPeerStatsConsensus)
+                        .thenApply(peers -> peers.stream()
+                                // filter, then sort
+                                .filter(networkService.peerFilter(filterDef))
+                                .map(peer -> savePeer(peer, false))
+                                .collect(Collectors.toList()))
+                        .thenApply(peers -> {
+                            logger.info(I18n.t("duniter4j.es.networkService.indexPeers.succeed", currencyName, firstPeer, peers.size(), (System.currentTimeMillis() - timeStart)));
+                            progressionModel.setStatus(ProgressionModel.Status.SUCCESS);
+                            return peers;
+                        });
+            } catch (InterruptedException | ExecutionException e) {
+                throw new TechnicalException("Error while loading peers: " + e.getMessage(), e);
+            }
+        } catch(Exception e) {
+            logger.error("Error during indexBlocksFromNode: " + e.getMessage(), e);
+            progressionModel.setStatus(ProgressionModel.Status.FAILED);
+        }
+
+        return this;
+    }
+
+/*
+    public void start(Peer peer, FilterAndSortSpec networkSpec) {
+        Preconditions.checkNotNull(networkSpec);
+        this.networkSpec = networkSpec;
+        start(peer);
+    }
+
+    public void start(Peer peer) {
+        Preconditions.checkNotNull(peer);
+
+        log.debug("Starting network crawler...");
+
+        addListeners(peer);
+
+        this.mainPeer = peer;
+
+        try {
+            this.peers = loadPeers(this.mainPeer).get();
+        }
+        catch(Exception e) {
+            throw new TechnicalException("Error during start load peers", e);
+        }
+
+        isStarted = true;
+        log.info("Network crawler started");
+    }
+
+    public void stop() {
+        if (!isStarted) return;
+        log.debug("Stopping network crawler...");
+
+        removeListeners();
+
+        this.mainPeer = null;
+        this.mainPeerWsEp = null;
+        this.isStarted = false;
+
+        this.executorService.shutdown();
+
+        log.info("Network crawler stopped");
+    }*/
+
+    /**
+     * Create or update a peer, depending on its existence and hash
+     * @param peer
+     * @param wait wait indexBlocksFromNode end
+     * @throws DuplicateIndexIdException
+     */
+    public Peer savePeer(final Peer peer, boolean wait) throws DuplicateIndexIdException {
+        Preconditions.checkNotNull(peer, "peer could not be null") ;
+        Preconditions.checkNotNull(peer.getCurrency(), "peer attribute 'currency' could not be null");
+        //Preconditions.checkNotNull(peer.getHash(), "peer attribute 'hash' could not be null");
+        Preconditions.checkNotNull(peer.getPubkey(), "peer attribute 'pubkey' could not be null");
+        Preconditions.checkNotNull(peer.getHost(), "peer attribute 'host' could not be null");
+        Preconditions.checkNotNull(peer.getApi(), "peer 'api' could not be null");
+
+        Peer existingPeer = getPeerByHash(peer.getCurrency(), peer.getHash());
+
+        // Currency not exists, or has changed, so create it
+        if (existingPeer == null) {
+            if (logger.isTraceEnabled()) {
+                logger.trace(String.format("Insert new peer [%s]", peer));
+            }
+
+            // Index new peer
+            indexPeer(peer, wait);
+        }
+
+        // Update existing peer
+        else {
+            logger.trace(String.format("Update peer [%s]", peer));
+            updatePeer(peer, wait);
+        }
+        return peer;
+    }
+
+    public void indexPeer(Peer peer, boolean wait) {
+        Preconditions.checkNotNull(peer);
+        Preconditions.checkArgument(StringUtils.isNotBlank(peer.getCurrency()));
+        Preconditions.checkNotNull(peer.getHash());
+        Preconditions.checkNotNull(peer.getHost());
+        Preconditions.checkNotNull(peer.getApi());
+
+        // Serialize into JSON
+        // WARN: must use GSON, to have same JSON result (e.g identities and joiners field must be converted into String)
+        try {
+            String json = objectMapper.writeValueAsString(peer);
+
+            // Preparing indexBlocksFromNode
+            IndexRequestBuilder indexRequest = client.prepareIndex(peer.getCurrency(), PEER_TYPE)
+                    .setId(peer.getHash())
+                    .setSource(json);
+
+            // Execute indexBlocksFromNode
+            ActionFuture<IndexResponse> futureResponse = indexRequest
+                    .setRefresh(true)
+                    .execute();
+
+            if (wait) {
+                futureResponse.actionGet();
+            }
+        }
+        catch(JsonProcessingException e) {
+            throw new TechnicalException(e);
+        }
+    }
+
+    public void updatePeer(Peer peer, boolean wait) {
+        Preconditions.checkNotNull(peer);
+        Preconditions.checkArgument(StringUtils.isNotBlank(peer.getCurrency()));
+        Preconditions.checkNotNull(peer.getHash());
+        Preconditions.checkNotNull(peer.getHost());
+        Preconditions.checkNotNull(peer.getApi());
+
+        // Serialize into JSON
+        // WARN: must use GSON, to have same JSON result (e.g identities and joiners field must be converted into String)
+        try {
+            String json = objectMapper.writeValueAsString(peer);
+
+            // Preparing indexBlocksFromNode
+            UpdateRequestBuilder updateRequest = client.prepareUpdate(peer.getCurrency(), PEER_TYPE, peer.getHash())
+                    .setDoc(json);
+
+            // Execute indexBlocksFromNode
+            ActionFuture<UpdateResponse> futureResponse = updateRequest
+                    .setRefresh(true)
+                    .execute();
+
+            if (wait) {
+                futureResponse.actionGet();
+            }
+        }
+        catch(JsonProcessingException e) {
+            throw new TechnicalException(e);
+        }
+    }
+
+    /**
+     *
+     * @param currencyName
+     * @param number the peer hash
+     * @param json block as JSON
+     */
+    public NetworkService indexPeerFromJson(String currencyName, int number, byte[] json, boolean refresh, boolean wait) {
+        Preconditions.checkNotNull(json);
+        Preconditions.checkArgument(json.length > 0);
+
+        // Preparing indexBlocksFromNode
+        IndexRequestBuilder indexRequest = client.prepareIndex(currencyName, PEER_TYPE)
+                .setId(String.valueOf(number))
+                .setRefresh(refresh)
+                .setSource(json);
+
+        // Execute indexBlocksFromNode
+        if (!wait) {
+            indexRequest.execute();
+        }
+        else {
+            indexRequest.execute().actionGet();
+        }
+
+        return this;
+    }
+
+    /**
+     * Index the given block, as the last (current) block. This will check is a fork has occur, and apply a rollback so.
+     */
+    public void onNetworkChanged() {
+        logger.info("ES network service -> peers changed: TODO: index new peers");
+    }
+
+    /**
+     *
+     * @param json block as json
+     * @param refresh Enable ES update with 'refresh' tag ?
+     * @param wait need to wait until processed ?
+     */
+    public NetworkService indexPeer(Peer peer, String json, boolean refresh, boolean wait) {
+        Preconditions.checkNotNull(json);
+        Preconditions.checkArgument(json.length() > 0);
+
+        String currencyName = blockCurrencyParser.getValueAsString(json);
+        String hash = blockHashParser.getValueAsString(json);
+
+        logger.info(I18n.t("duniter4j.es.networkService.indexPeer", currencyName, peer));
+        if (logger.isTraceEnabled()) {
+            logger.trace(json);
+        }
+
+
+        // Preparing index
+        IndexRequestBuilder indexRequest = client.prepareIndex(currencyName, PEER_TYPE)
+                .setId(hash)
+                .setRefresh(refresh)
+                .setSource(json);
+
+        // Execute indexBlocksFromNode
+        if (!wait) {
+            indexRequest.execute();
+        }
+        else {
+            indexRequest.execute().actionGet();
+        }
+
+        return this;
+    }
+
+    public List<Peer> findPeersByHash(String currencyName, String query) {
+        String[] queryParts = query.split("[\\t ]+");
+
+        // Prepare request
+        SearchRequestBuilder searchRequest = client
+                .prepareSearch(currencyName)
+                .setTypes(PEER_TYPE)
+                .setSearchType(SearchType.DFS_QUERY_THEN_FETCH);
+
+        // If only one term, search as prefix
+        if (queryParts.length == 1) {
+            searchRequest.setQuery(QueryBuilders.prefixQuery("hash", query));
+        }
+
+        // If more than a word, search on terms match
+        else {
+            searchRequest.setQuery(QueryBuilders.matchQuery("hash", query));
+        }
+
+        // Sort as score/memberCount
+        searchRequest.addSort("_score", SortOrder.DESC)
+                .addSort("number", SortOrder.DESC);
+
+        // Highlight matched words
+        searchRequest.setHighlighterTagsSchema("styled")
+                .addHighlightedField("hash")
+                .addFields("hash")
+                .addFields("*", "_source");
+
+        // Execute query
+        SearchResponse searchResponse = searchRequest.execute().actionGet();
+
+        // Read query result
+        return toBlocks(searchResponse, true);
+    }
+
+    /* -- Internal methods -- */
+
+    public static XContentBuilder createPeerTypeMapping() {
+        try {
+            XContentBuilder mapping = XContentFactory.jsonBuilder()
+                    .startObject()
+                    .startObject(PEER_TYPE)
+                    .startObject("properties")
+
+                    // currency
+                    .startObject("currency")
+                    .field("sortType", "string")
+                    .endObject()
+
+                    // pubkey
+                    .startObject("pubkey")
+                    .field("sortType", "string")
+                    .field("index", "not_analyzed")
+                    .endObject()
+
+                    // api
+                    .startObject("api")
+                    .field("sortType", "string")
+                    .field("index", "not_analyzed")
+                    .endObject()
+
+                    // uid
+                    .startObject("uid")
+                    .field("sortType", "string")
+                    .endObject()
+
+                    // dns
+                    .startObject("dns")
+                    .field("sortType", "string")
+                    .endObject()
+
+                    // ipv4
+                    .startObject("ipv4")
+                    .field("sortType", "string")
+                    .endObject()
+
+                    // ipv6
+                    .startObject("ipv6")
+                    .field("sortType", "string")
+                    .endObject()
+
+                    .endObject()
+                    .endObject().endObject();
+
+            return mapping;
+        }
+        catch(IOException ioe) {
+            throw new TechnicalException("Error while getting mapping for peer index: " + ioe.getMessage(), ioe);
+        }
+    }
+
+    public Peer getPeerByHash(String currencyName, String hash) {
+
+        // Prepare request
+        SearchRequestBuilder searchRequest = client
+                .prepareSearch(currencyName)
+                .setTypes(PEER_TYPE)
+                .setSearchType(SearchType.DFS_QUERY_THEN_FETCH);
+
+        // If more than a word, search on terms match
+        searchRequest.setQuery(QueryBuilders.matchQuery("_id", hash));
+
+        // Execute query
+        try {
+            SearchResponse searchResponse = searchRequest.execute().actionGet();
+            List<Peer> blocks = toBlocks(searchResponse, false);
+            if (CollectionUtils.isEmpty(blocks)) {
+                return null;
+            }
+
+            // Return the unique result
+            return CollectionUtils.extractSingleton(blocks);
+        }
+        catch(JsonSyntaxException e) {
+            throw new TechnicalException(String.format("Error while getting indexed peer #%s for [%s]", hash, currencyName), e);
+        }
+
+    }
+
+    protected List<Peer> toBlocks(SearchResponse response, boolean withHighlight) {
+        // Read query result
+        List<Peer> result = Lists.newArrayList();
+        response.getHits().forEach(searchHit -> {
+            Peer peer;
+            if (searchHit.source() != null) {
+                String jsonString = new String(searchHit.source());
+                try {
+                    peer = objectMapper.readValue(jsonString, Peer.class);
+                } catch(Exception e) {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Error while parsing peer from JSON:\n" + jsonString);
+                    }
+                    throw new JsonSyntaxException("Error while read peer from JSON: " + e.getMessage(), e);
+                }
+            }
+            else {
+                peer = new Peer();
+                SearchHitField field = searchHit.getFields().get("hash");
+                peer.setHash(field.getValue());
+            }
+            result.add(peer);
+
+            // If possible, use highlights
+            if (withHighlight) {
+                Map<String, HighlightField> fields = searchHit.getHighlightFields();
+                for (HighlightField field : fields.values()) {
+                    String blockNameHighLight = field.getFragments()[0].string();
+                    peer.setHash(blockNameHighLight);
+                }
+            }
+        });
+
+        return result;
+    }
+
+
+    protected void reportIndexPeersProgress(ProgressionModel progressionModel, String currencyName, Peer peer, int offset, int total) {
+        int pct = offset * 100 / total;
+        progressionModel.setCurrent(pct);
+
+        progressionModel.setMessage(I18n.t("duniter4j.es.networkService.indexPeers.progress", currencyName, peer, offset, pct));
+        if (logger.isInfoEnabled()) {
+            logger.info(I18n.t("duniter4j.es.networkService.indexPeers.progress", currencyName, peer, offset, pct));
+        }
+
+    }
+
+}
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java
index 23609da1..b264f91d 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java
@@ -33,17 +33,15 @@ import org.duniter.core.client.service.DataContext;
 import org.duniter.core.client.service.HttpService;
 import org.duniter.core.client.service.HttpServiceImpl;
 import org.duniter.core.client.service.bma.*;
+import org.duniter.core.client.service.local.*;
+import org.duniter.core.client.service.local.NetworkService;
 import org.duniter.core.client.service.local.CurrencyService;
-import org.duniter.core.client.service.local.CurrencyServiceImpl;
-import org.duniter.core.client.service.local.PeerService;
-import org.duniter.core.client.service.local.PeerServiceImpl;
 import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.service.CryptoService;
 import org.duniter.core.service.Ed25519CryptoServiceImpl;
 import org.duniter.core.service.MailService;
 import org.duniter.core.service.MailServiceImpl;
 import org.elasticsearch.common.inject.Inject;
-import org.elasticsearch.common.inject.Injector;
 import org.elasticsearch.common.inject.Singleton;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.ESLoggerFactory;
@@ -95,6 +93,7 @@ public class ServiceLocator
                 .bind(MailService.class, MailServiceImpl.class)
                 .bind(PeerService.class, PeerServiceImpl.class)
                 .bind(CurrencyService.class, CurrencyServiceImpl.class)
+                .bind(NetworkService.class, NetworkServiceImpl.class)
                 .bind(HttpService.class, HttpServiceImpl.class)
                 .bind(CurrencyDao.class, MemoryCurrencyDaoImpl.class)
                 .bind(PeerDao.class, MemoryPeerDaoImpl.class)
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceModule.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceModule.java
index fd377ed5..68d7ccbb 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceModule.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceModule.java
@@ -52,8 +52,9 @@ public class ServiceModule extends AbstractModule implements Module {
         bind(PluginInit.class).asEagerSingleton();
         bind(ChangeService.class).asEagerSingleton();
 
-        // indexation services
+        // blockchain indexation services
         bind(BlockchainService.class).asEagerSingleton();
+        bind(NetworkService.class).asEagerSingleton();
 
         // Duniter Client API beans
         bindWithLocator(BlockchainRemoteService.class);
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java
index 4818cbf5..3025a909 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java
@@ -98,7 +98,7 @@ public class ThreadPool extends AbstractLifecycleComponent<ThreadPool> {
     public void doClose() {}
 
     /**
-     * Schedules an rest when node is started (all services and modules ready)
+     * Schedules an rest when node is started (allOfToList services and modules ready)
      *
      * @param job the rest to execute when node started
      * @return a ScheduledFuture who's get will return when the task is complete and throw an exception if it is canceled
@@ -233,4 +233,7 @@ public class ThreadPool extends AbstractLifecycleComponent<ThreadPool> {
         return canContinue;
     }
 
+    public ScheduledExecutorService scheduler() {
+        return delegate.scheduler();
+    }
 }
diff --git a/duniter4j-es-core/src/main/resources/META-INF/services/org.duniter.core.beans.Bean b/duniter4j-es-core/src/main/resources/META-INF/services/org.duniter.core.beans.Bean
index 0f39d761..1cf6cb3c 100644
--- a/duniter4j-es-core/src/main/resources/META-INF/services/org.duniter.core.beans.Bean
+++ b/duniter4j-es-core/src/main/resources/META-INF/services/org.duniter.core.beans.Bean
@@ -9,6 +9,7 @@ org.duniter.core.client.service.HttpServiceImpl
 org.duniter.core.client.service.DataContext
 org.duniter.core.client.service.local.PeerServiceImpl
 org.duniter.core.client.service.local.CurrencyServiceImpl
+org.duniter.core.client.service.local.NetworkServiceImpl
 org.duniter.core.client.dao.mem.MemoryCurrencyDaoImpl
 org.duniter.core.client.dao.mem.MemoryPeerDaoImpl
 
diff --git a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties
index a6067b35..f7bd9849 100644
--- a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties
+++ b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties
@@ -34,6 +34,11 @@ duniter4j.config.option.tasks.queueCapacity.description=
 duniter4j.config.option.temp.directory.description=
 duniter4j.config.option.version.description=
 duniter4j.config.parse.error=
+duniter4j.es.networkService.indexPeer=
+duniter4j.es.networkService.indexPeers.progress=
+duniter4j.es.networkService.indexPeers.remoteParametersError=
+duniter4j.es.networkService.indexPeers.succeed=
+duniter4j.es.networkService.indexPeers.task=
 duniter4j.executor.task.waitingExecution=
 duniter4j.job.stopped=
 duniter4j.job.stopping=
diff --git a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties
index 7e587779..f8468a74 100644
--- a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties
+++ b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties
@@ -34,6 +34,11 @@ duniter4j.config.option.tasks.queueCapacity.description=
 duniter4j.config.option.temp.directory.description=
 duniter4j.config.option.version.description=
 duniter4j.config.parse.error=
+duniter4j.es.networkService.indexPeer=[%s] Indexing peer [%s]...
+duniter4j.es.networkService.indexPeers.progress=[%s] [%s] Indexing peers (%s%%)...
+duniter4j.es.networkService.indexPeers.remoteParametersError=[%s] Error when calling [/blockchain/parameters]\: %s
+duniter4j.es.networkService.indexPeers.succeed=[%s] [%s] All peers indexed\: found [%s] in [%s ms]
+duniter4j.es.networkService.indexPeers.task=[%s] [%s] Indexing peers...
 duniter4j.executor.task.waitingExecution=
 duniter4j.job.stopped=
 duniter4j.job.stopping=
diff --git a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/CitiesRegistryService.java b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/CitiesRegistryService.java
index dadcc55a..b4b30dfb 100644
--- a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/CitiesRegistryService.java
+++ b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/CitiesRegistryService.java
@@ -105,7 +105,7 @@ public class CitiesRegistryService extends AbstractService {
 
     public void initCities() {
         if (log.isDebugEnabled()) {
-            log.debug("Initializing all registry cities");
+            log.debug("Initializing allOfToList registry cities");
         }
 
         //File bulkFile = createCitiesBulkFile2();
diff --git a/pom.xml b/pom.xml
index 3896935d..c5c5a9db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,6 +104,7 @@
     <module>duniter4j-es-user</module>
     <module>duniter4j-es-gchange</module>
     <module>duniter4j-es-assembly</module>
+      <module>duniter4j-cmd</module>
   </modules>
 
   <scm>
@@ -408,7 +409,7 @@
                 <!-- This is need to override the option version, in configuration classes -->
                 <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                 <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
-                <!-- Main class, configured in sub-modules -->
+                <!-- fr.duniter.cmd.Main class, configured in sub-modules -->
                 <mainClass>${maven.jar.main.class}</mainClass>
               </manifest>
               <manifestEntries>
-- 
GitLab