diff --git a/duniter4j-client/pom.xml b/duniter4j-client/pom.xml
index 4a32116a5a1f76eef0813080e32a0b8766615051..94dbf2284d8dfa1ed19ccc67a10b4bcc1595b9e3 100644
--- a/duniter4j-client/pom.xml
+++ b/duniter4j-client/pom.xml
@@ -15,13 +15,14 @@
     <properties>
         <jTextUtilsVersion>0.3.3</jTextUtilsVersion>
         <opencsvVersion>2.3</opencsvVersion>
+        <jansiVersion>1.15</jansiVersion>
 
         <!-- i18n configuration -->
         <i18n.bundleOutputName>duniter4j-client-i18n</i18n.bundleOutputName>
         <i18n.bundleCsvFile>${i18n.bundleOutputName}.csv</i18n.bundleCsvFile>
 
         <maven.jar.main.class>
-            fr.duniter.client.Main
+            org.duniter.client.Main
         </maven.jar.main.class>
 
         <bundlePrefix>duniter4j-client-${project.version}</bundlePrefix>
@@ -79,10 +80,11 @@
             <version>${opencsvVersion}</version>
         </dependency>
 
+
         <dependency>
-            <groupId>org.jline</groupId>
-            <artifactId>jline</artifactId>
-            <version>3.2.0</version>
+            <groupId>org.fusesource.jansi</groupId>
+            <artifactId>jansi</artifactId>
+            <version>${jansiVersion}</version>
         </dependency>
     </dependencies>
 
diff --git a/duniter4j-client/src/main/assembly/min/duniter4j.sh b/duniter4j-client/src/main/assembly/min/duniter4j.sh
index 344b7600978c3d01a7e60a0b07d288b614201109..4ddc4538646a981abccbe62782343c39302d7ccc 100644
--- a/duniter4j-client/src/main/assembly/min/duniter4j.sh
+++ b/duniter4j-client/src/main/assembly/min/duniter4j.sh
@@ -22,7 +22,7 @@ export JAR="$JARDIR/${project.build.finalName}.${project.packaging}"
 export I18N_DIR="$APPDIR/i18n"
 
 # Retrieve the JAVA installation
-if [ "~$JAVA_HOME" -eq "~" ]; then
+if [ "$JAVA_HOME~" == "~" ]; then
     export JAVA_HOME="$APPDIR/jre"
     export JAVA_COMMAND="$JAVA_HOME/bin/java"
 
diff --git a/duniter4j-client/src/main/filtered-resources/log4j.properties b/duniter4j-client/src/main/filtered-resources/log4j.properties
index 998366881a6063fc16080ec9508af9c2b2333b2b..135fd9c4a4d3509072c55b024bace0abee7421fa 100644
--- a/duniter4j-client/src/main/filtered-resources/log4j.properties
+++ b/duniter4j-client/src/main/filtered-resources/log4j.properties
@@ -18,7 +18,7 @@ log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %5p %c - %m%n
 
 # Duniter4j levels
 log4j.logger.org.duniter=INFO
-log4j.logger.org.duniter.core.beans=WARN
+log4j.logger.org.duniter.core=WARN
 # Avoid warning on leaf not found (Duniter issue)
 log4j.logger.org.duniter.core.client.service.local.NetworkServiceImpl=ERROR
 
diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/ClearableConsole.java b/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/ClearableConsole.java
deleted file mode 100644
index 168bdc6f35870c1b504854c66bf49c3b58c17efb..0000000000000000000000000000000000000000
--- a/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/ClearableConsole.java
+++ /dev/null
@@ -1,214 +0,0 @@
-package fr.duniter.client.actions.utils;
-
-/*
- * #%L
- * Duniter4j :: Client
- * %%
- * Copyright (C) 2014 - 2017 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.beust.jcommander.internal.Maps;
-import org.duniter.core.util.Preconditions;
-
-import java.io.PrintStream;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Created by blavenie on 28/03/17.
- */
-public class ClearableConsole extends PrintStream {
-
-    public interface Color {
-        int black = 30;
-        int red = 31;
-        int green = 32;
-        int brown = 33;
-        int blue = 34;
-        int magenta = 35;
-        int cyan = 36;
-        int lightgray = 37;
-    }
-
-    private int rowsCount = 0;
-
-    private Map<Pattern, Integer> regexColors = Maps.newHashMap();
-
-    public ClearableConsole() {
-        super(System.out, true);
-    }
-
-    public ClearableConsole(PrintStream delegate) {
-        super(delegate, true);
-    }
-
-    public void clearConsole() {
-        if (rowsCount == 0) {
-            super.print("\033[H\033[2J");
-            super.flush();
-        }
-        else {
-            moveCursor(rowsCount);
-        }
-        rowsCount = 0;
-    }
-
-    public ClearableConsole putRegexColor(String regex, int color) {
-        Preconditions.checkArgument(color >= 30 && color <= 37);
-
-        regexColors.put(Pattern.compile(regex), color);
-        return this;
-    }
-
-    public ClearableConsole removeRegex(String regex) {
-
-        regexColors.remove(Pattern.compile(regex));
-        return this;
-    }
-
-    public void moveCursor(int nbLinesUp) {
-        for (int i = 1; i <= nbLinesUp; i++) {
-            super.print("\033[1A"); // Move up
-            super.print("\033[2K"); // Erase line content
-        }
-
-        super.flush();
-    }
-
-    @Override
-    public void println(boolean x) {
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void println(char x) {
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void println(int x) {
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void println(long x) {
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void println(float x) {
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void println(double x) {
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void println(char[] x) {
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void println(String x) {
-
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void println(Object x) {
-        super.println(x);
-        rowsCount++;
-    }
-
-    @Override
-    public void print(char c) {
-        super.print(c);
-    }
-
-    @Override
-    public void print(char[] x) {
-        super.print(x);
-        for (int i=0; i< x.length; i++) {
-            if (x[i] == '\n') {
-                rowsCount++;
-            }
-        }
-    }
-
-    @Override
-    public void print(String s) {
-
-        for (Pattern pattern: regexColors.keySet()) {
-            Matcher matcher = pattern.matcher(s);
-            if (matcher.find()) {
-                StringBuilder sb = new StringBuilder();
-                if (matcher.start() > 0) {
-                    sb.append(s.substring(0, matcher.start()));
-                }
-                sb.append(String.format("\033[0;%sm", regexColors.get(pattern)));
-                sb.append(s.substring(matcher.start(), matcher.end()));
-                sb.append("\033[0m");
-                if (matcher.end() < s.length()) {
-                    sb.append(s.substring(matcher.end()));
-                }
-                s = sb.toString();
-            }
-        }
-        super.print(s);
-        for (int i=0; i< s.length(); i++) {
-            if (s.charAt(i) == '\n') {
-                rowsCount++;
-            }
-        }
-    }
-
-    @Override
-    public void print(Object obj) {
-        String s = String.valueOf(obj);
-        print(s);
-    }
-
-    @Override
-    public PrintStream append(CharSequence csq, int start, int end) {
-        for (int i=start; i<=end; i++) {
-            if (csq.charAt(i) == '\n') {
-                rowsCount++;
-            }
-        }
-        return super.append(csq, start, end);
-    }
-
-    @Override
-    public PrintStream append(char c) {
-        if (c == '\n') {
-            rowsCount++;
-        }
-        return super.append(c);
-    }
-}
diff --git a/duniter4j-client/src/main/java/fr/duniter/client/Main.java b/duniter4j-client/src/main/java/org/duniter/client/Main.java
similarity index 96%
rename from duniter4j-client/src/main/java/fr/duniter/client/Main.java
rename to duniter4j-client/src/main/java/org/duniter/client/Main.java
index ff48c43f7aebeeb8dcc8686f64f9cda1553a88cd..c7b1c7258314d38bffd860888e06d61e1f7f2cbf 100644
--- a/duniter4j-client/src/main/java/fr/duniter/client/Main.java
+++ b/duniter4j-client/src/main/java/org/duniter/client/Main.java
@@ -1,4 +1,4 @@
-package fr.duniter.client;
+package org.duniter.client;
 
 /*
  * #%L
@@ -27,15 +27,13 @@ import com.beust.jcommander.Parameter;
 import com.beust.jcommander.ParameterException;
 import com.beust.jcommander.Parameters;
 import com.google.common.collect.Lists;
-import fr.duniter.client.actions.NetworkAction;
-import fr.duniter.client.actions.TransactionAction;
+import org.duniter.client.actions.NetworkAction;
+import org.duniter.client.actions.TransactionAction;
 import org.apache.commons.io.FileUtils;
 import org.duniter.core.client.config.Configuration;
 import org.duniter.core.client.config.ConfigurationOption;
 import org.duniter.core.client.service.ServiceLocator;
 import org.duniter.core.util.StringUtils;
-import org.jline.terminal.Terminal;
-import org.jline.terminal.TerminalBuilder;
 import org.nuiton.i18n.I18n;
 import org.nuiton.i18n.init.DefaultI18nInitializer;
 import org.nuiton.i18n.init.UserI18nInitializer;
diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/AbstractAction.java b/duniter4j-client/src/main/java/org/duniter/client/actions/AbstractAction.java
similarity index 97%
rename from duniter4j-client/src/main/java/fr/duniter/client/actions/AbstractAction.java
rename to duniter4j-client/src/main/java/org/duniter/client/actions/AbstractAction.java
index 796f52d8fbdac15f521f412d5a6b617c8d70eee6..35cee1f07fc4a9a14b8e487a87d761494fbbe0d1 100644
--- a/duniter4j-client/src/main/java/fr/duniter/client/actions/AbstractAction.java
+++ b/duniter4j-client/src/main/java/org/duniter/client/actions/AbstractAction.java
@@ -1,4 +1,4 @@
-package fr.duniter.client.actions;
+package org.duniter.client.actions;
 
 /*
  * #%L
diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/NetworkAction.java b/duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java
similarity index 62%
rename from duniter4j-client/src/main/java/fr/duniter/client/actions/NetworkAction.java
rename to duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java
index 171326abf2c5716957b6224cfc19edcf50f445cc..b2c64093006593e6e1c1dc035264018078c19738 100644
--- a/duniter4j-client/src/main/java/fr/duniter/client/actions/NetworkAction.java
+++ b/duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java
@@ -1,4 +1,4 @@
-package fr.duniter.client.actions;
+package org.duniter.client.actions;
 
 /*
  * #%L
@@ -25,24 +25,22 @@ package fr.duniter.client.actions;
 import com.beust.jcommander.Parameter;
 import com.beust.jcommander.Parameters;
 import com.beust.jcommander.ParametersDelegate;
-import com.beust.jcommander.internal.Lists;
 import dnl.utils.text.table.TextTable;
-import fr.duniter.client.actions.params.PeerParameters;
-import fr.duniter.client.actions.utils.ClearableConsole;
-import fr.duniter.client.actions.utils.Formatters;
+import org.duniter.client.actions.params.PeerParameters;
+import org.duniter.client.actions.utils.RegexAnsiConsole;
+import org.duniter.client.actions.utils.Formatters;
 import org.apache.commons.io.IOUtils;
 import org.duniter.core.client.model.local.Peer;
 import org.duniter.core.client.service.ServiceLocator;
-import org.duniter.core.client.service.bma.BlockchainRemoteService;
 import org.duniter.core.client.service.local.NetworkService;
 import org.duniter.core.util.CollectionUtils;
 import org.duniter.core.util.FileUtils;
+import org.fusesource.jansi.Ansi;
 import org.nuiton.i18n.I18n;
 
 import java.io.*;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -62,10 +60,9 @@ public class NetworkAction extends AbstractAction {
     @Parameter(names = "--output", description = "Output file (CSV format)", descriptionKey = "duniter4j.client.network.params.output")
     private File outputFile = null;
 
-    private ClearableConsole console;
+    private RegexAnsiConsole console;
 
     private DateFormat dateFormat;
-    private List<String> knownBlocks = Lists.newArrayList();
 
     public NetworkAction() {
         super();
@@ -74,81 +71,40 @@ public class NetworkAction extends AbstractAction {
     @Override
     public void run() {
 
+
         peerParameters.parse();
         final Peer mainPeer = peerParameters.getPeer();
         checkOutputFileIfNotNull(); // make sure the file (if any) is writable
 
         dateFormat = SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, I18n.getDefaultLocale());
 
-        console = new ClearableConsole(System.out)
-            .putRegexColor(I18n.t("duniter4j.client.network.ssl"), ClearableConsole.Color.green)
-            .putRegexColor(I18n.t("duniter4j.client.network.mirror"), ClearableConsole.Color.lightgray);
-
+        console = new RegexAnsiConsole();
         System.setOut(console);
 
         log.info(I18n.t("duniter4j.client.network.loadingPeers"));
-        List<Peer> peers = loadPeers(mainPeer);
-
-        showPeersTable(peers, true/*autoRefresh*/);
-
-        if (autoRefresh) {
-            BlockchainRemoteService bcService = ServiceLocator.instance().getBlockchainRemoteService();
 
-            peers.stream().forEach(peer -> {
-                String buid = peer.getStats().getBlockNumber() + "-" + peer.getStats().getBlockHash();
-                if (!knownBlocks.contains(buid)) {
-                    knownBlocks.add(buid);
-                }
-            });
+        NetworkService service = ServiceLocator.instance().getNetworkService();
 
-            // Start listening for new peer...
-            bcService.addPeerListener(mainPeer, message -> updatePeers(mainPeer, knownBlocks));
-            // Start listening for new block...
-            bcService.addBlockListener(mainPeer, message -> updatePeers(mainPeer, knownBlocks));
+        if (!autoRefresh) {
+            List<Peer> peers = service.getPeers(mainPeer);
+            showPeersTable(peers, false);
+        }
+        else {
+            service.addPeersChangeListener(mainPeer, peers -> showPeersTable(peers, true));
 
             try {
                 while(true) {
                     Thread.sleep(10000); // 10 s
-
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
 
-        // TODO: DEV only
-       /* else  {
-            try {
-                int blockCount = 1500;
-                while(true) {
-                    Thread.sleep(2000); // 2 s
-
-                    List<Peer> updatedPeers = new ArrayList<>();
-
-                    for (int i=0; i<5; i++) {
-                        Peer peer = Peer.newBuilder().setHost("p1").setPort(80)
-                                .build();
-                        peer.getStats().setBlockNumber(blockCount);
-                        updatedPeers.add(peer);
-                    }
-                    updatedPeers.addAll(peers);
-
-                    showPeersTable(updatedPeers, true);
-                    blockCount++;
-                }
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-        }*/
-
     }
 
     /* -- protected methods -- */
 
-    public List<Peer> loadPeers(Peer mainPeer) {
-        NetworkService service = ServiceLocator.instance().getNetworkService();
-        return service.getPeers(mainPeer);
-    }
 
     public void showPeersTable(List<Peer> peers, boolean clearConsole) {
 
@@ -163,18 +119,27 @@ public class NetworkAction extends AbstractAction {
         }
 
         Peer mainConsensusPeer = peers.get(0);
-        if (mainConsensusPeer.getStats().isMainConsensus()) {
-            Long mediantTime = mainConsensusPeer.getStats().getMedianTime();
-            if (mediantTime != null) {
-                console.println(I18n.t("duniter4j.client.network.medianTime",
-                        dateFormat.format(new Date(mediantTime * 1000))));
-            }
-
-            knownBlocks.stream().forEach(buid -> {
-                console.putRegexColor(Formatters.formatBuid(buid), ClearableConsole.Color.lightgray);
-            });
-
-            console.putRegexColor(formatBuid(mainConsensusPeer.getStats()), ClearableConsole.Color.green);
+        Peer.Stats mainConsensusStats = mainConsensusPeer.getStats();
+        if (mainConsensusStats.isMainConsensus()) {
+            Long mediantTime = mainConsensusStats.getMedianTime();
+            String mainBuid = formatBuid(mainConsensusStats);
+
+            console.resetFgRegexps()
+                   .fgRegexp(I18n.t("duniter4j.client.network.ssl"), Ansi.Color.MAGENTA)
+                   .fgRegexp(I18n.t("duniter4j.client.network.mirror"), Ansi.Color.CYAN)
+                   .fgRegexp(mainBuid, Ansi.Color.GREEN);
+
+            peers.stream()
+                    .filter(peer -> peer.getStats().isForkConsensus())
+                    .map(peer -> formatBuid(peer.getStats()))
+                    .forEach(forkConsensusBuid -> console.fgRegexp(Formatters.formatBuid(forkConsensusBuid), Ansi.Color.YELLOW));
+
+            // Log blockchain info
+            console.println("\t" + I18n.t("duniter4j.client.network.header",
+                    mainBuid,
+                    dateFormat.format(new Date(mediantTime * 1000)),
+                    mainConsensusStats.getConsensusPct()
+            ));
         }
 
         String[] columnNames = {
@@ -208,7 +173,6 @@ public class NetworkAction extends AbstractAction {
             rows[i++] = row;
         }
 
-
         TextTable tt = new TextTable(columnNames, rows);
 
         // Write result to filCSV
@@ -256,25 +220,8 @@ public class NetworkAction extends AbstractAction {
         }
     }
 
-    protected void updatePeers(Peer mainPeer, List<String> knownBlocks) {
-        List<Peer> updatedPeers = loadPeers(mainPeer);
-
-        int knowBlockSize = knownBlocks.size();
-        updatedPeers.stream().forEach(peer -> {
-            String buid = peer.getStats().getBlockNumber() + "-" + peer.getStats().getBlockHash();
-            if (!knownBlocks.contains(buid)) {
-                knownBlocks.add(buid);
-            }
-        });
-
-        // new block received: refresh console
-        if (knowBlockSize < knownBlocks.size()) {
-            showPeersTable(updatedPeers, true);
-        }
-    }
-
     protected void clearConsole() {
-        console.clearConsole();
+        console.eraseScreen();
     }
 
     protected String formatBuid(Peer.Stats stats) {
diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/TransactionAction.java b/duniter4j-client/src/main/java/org/duniter/client/actions/TransactionAction.java
similarity index 97%
rename from duniter4j-client/src/main/java/fr/duniter/client/actions/TransactionAction.java
rename to duniter4j-client/src/main/java/org/duniter/client/actions/TransactionAction.java
index 77c3f121b078ea1b2bf9bd655f5f702f77e5dad1..d8ee63e11057b7ec20b8d6832dd8c1a8a0bf3c59 100644
--- a/duniter4j-client/src/main/java/fr/duniter/client/actions/TransactionAction.java
+++ b/duniter4j-client/src/main/java/org/duniter/client/actions/TransactionAction.java
@@ -1,4 +1,4 @@
-package fr.duniter.client.actions;
+package org.duniter.client.actions;
 
 /*
  * #%L
@@ -28,9 +28,9 @@ import com.beust.jcommander.Parameters;
 import com.beust.jcommander.ParametersDelegate;
 import com.beust.jcommander.validators.PositiveInteger;
 import com.google.common.collect.ImmutableList;
-import fr.duniter.client.actions.params.AuthParameters;
-import fr.duniter.client.actions.params.PeerParameters;
-import fr.duniter.client.actions.utils.Formatters;
+import org.duniter.client.actions.params.AuthParameters;
+import org.duniter.client.actions.params.PeerParameters;
+import org.duniter.client.actions.utils.Formatters;
 import org.duniter.core.client.model.bma.EndpointApi;
 import org.duniter.core.client.model.local.Currency;
 import org.duniter.core.client.model.local.Peer;
diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/params/AuthParameters.java b/duniter4j-client/src/main/java/org/duniter/client/actions/params/AuthParameters.java
similarity index 98%
rename from duniter4j-client/src/main/java/fr/duniter/client/actions/params/AuthParameters.java
rename to duniter4j-client/src/main/java/org/duniter/client/actions/params/AuthParameters.java
index a8311da002f40673737cb5e652a5b8541c05b07b..0991711981397d2f71fadbad20fb81e06eb09c4d 100644
--- a/duniter4j-client/src/main/java/fr/duniter/client/actions/params/AuthParameters.java
+++ b/duniter4j-client/src/main/java/org/duniter/client/actions/params/AuthParameters.java
@@ -1,4 +1,4 @@
-package fr.duniter.client.actions.params;
+package org.duniter.client.actions.params;
 
 /*
  * #%L
diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/params/PeerParameters.java b/duniter4j-client/src/main/java/org/duniter/client/actions/params/PeerParameters.java
similarity index 98%
rename from duniter4j-client/src/main/java/fr/duniter/client/actions/params/PeerParameters.java
rename to duniter4j-client/src/main/java/org/duniter/client/actions/params/PeerParameters.java
index 4f0b12a291fdf3b934ebca503c456307088d854e..665915442bc9f2a1a5661b761b39f3c684015dcd 100644
--- a/duniter4j-client/src/main/java/fr/duniter/client/actions/params/PeerParameters.java
+++ b/duniter4j-client/src/main/java/org/duniter/client/actions/params/PeerParameters.java
@@ -1,4 +1,4 @@
-package fr.duniter.client.actions.params;
+package org.duniter.client.actions.params;
 
 /*
  * #%L
diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/Formatters.java b/duniter4j-client/src/main/java/org/duniter/client/actions/utils/Formatters.java
similarity index 98%
rename from duniter4j-client/src/main/java/fr/duniter/client/actions/utils/Formatters.java
rename to duniter4j-client/src/main/java/org/duniter/client/actions/utils/Formatters.java
index 57ef912edfec1c8682f67536e27b296b52ccff5c..55530b33ef61753b1dd35c2fff0be82e69e6db38 100644
--- a/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/Formatters.java
+++ b/duniter4j-client/src/main/java/org/duniter/client/actions/utils/Formatters.java
@@ -1,4 +1,4 @@
-package fr.duniter.client.actions.utils;
+package org.duniter.client.actions.utils;
 
 /*
  * #%L
diff --git a/duniter4j-client/src/main/java/org/duniter/client/actions/utils/RegexAnsiConsole.java b/duniter4j-client/src/main/java/org/duniter/client/actions/utils/RegexAnsiConsole.java
new file mode 100644
index 0000000000000000000000000000000000000000..f5098565d45983d5b3dc5320d19560af3a50a0c6
--- /dev/null
+++ b/duniter4j-client/src/main/java/org/duniter/client/actions/utils/RegexAnsiConsole.java
@@ -0,0 +1,224 @@
+package org.duniter.client.actions.utils;
+
+/*
+ * #%L
+ * Duniter4j :: Client
+ * %%
+ * Copyright (C) 2014 - 2017 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.beust.jcommander.internal.Maps;
+import org.fusesource.jansi.Ansi;
+import org.fusesource.jansi.AnsiConsole;
+
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.fusesource.jansi.Ansi.Erase;
+import static org.fusesource.jansi.Ansi.ansi;
+
+/**
+ * Created by blavenie on 28/03/17.
+ */
+public class RegexAnsiConsole extends PrintStream {
+
+    private Long rowsCount = 0l;
+    private Map<Pattern, Ansi.Color> fgRegexps = Maps.newHashMap();
+
+    public RegexAnsiConsole(OutputStream delegate) {
+        super(AnsiConsole.wrapOutputStream(delegate), true);
+    }
+
+    public RegexAnsiConsole() {
+        super(AnsiConsole.out(), true);
+    }
+
+    public RegexAnsiConsole eraseScreen() {
+        Ansi ansi = ansi();
+        synchronized (rowsCount) {
+            // If first call to erase: clean screen then save cursor position
+            if (rowsCount == 0) {
+                ansi.eraseScreen()
+                        .cursor(0,0)
+                        .saveCursorPosition();
+            }
+            else {
+                // Try to erase writtent lines
+                for (int i = 1; i <= rowsCount; i++) {
+                    ansi.cursorUp(1).eraseLine(Erase.ALL);
+                }
+
+                // Reset rowcount
+                rowsCount = 0l;
+
+                // Make sure to go back to saved cursor point
+                // Then clean screen again
+                ansi.restoreCursorPosition()
+                        .eraseScreen();
+            }
+
+        }
+
+        super.print(ansi);
+        return this;
+    }
+    
+    public RegexAnsiConsole resetFgRegexps() {
+        fgRegexps.clear();
+        return this;
+    }
+
+    public RegexAnsiConsole fgRegexp(String regex, Ansi.Color color) {
+        fgRegexps.put(Pattern.compile(regex), color);
+        return this;
+    }
+
+    @Override
+    public void print(String s) {
+
+        for (Pattern pattern: fgRegexps.keySet()) {
+            Matcher matcher = pattern.matcher(s);
+            if (matcher.find()) {
+                Ansi.Color fgColor = fgRegexps.get(pattern);
+                Ansi ansi = ansi();
+                if (matcher.start() > 0) {
+                    ansi.a(s.substring(0, matcher.start()));
+                }
+
+                ansi.fg(fgColor)
+                        .a(s.substring(matcher.start(), matcher.end()))
+                        .reset();
+                if (matcher.end() < s.length()) {
+                    ansi.a(s.substring(matcher.end()));
+                }
+                s = ansi.toString();
+            }
+        }
+
+        long newLineCount = 0;
+        for (int i=0; i<s.length(); i++) {
+            if (s.charAt(i) == '\n') newLineCount++;
+        }
+        incRowCount(newLineCount);
+
+        super.print(s);
+    }
+
+    @Override
+    public void println(boolean x) {
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void println(char x) {
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void println(int x) {
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void println(long x) {
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void println(float x) {
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void println(double x) {
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void println(char[] x) {
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void println(String x) {
+
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void println(Object x) {
+        super.println(x);
+        incRowCount();
+    }
+
+    @Override
+    public void print(char c) {
+        super.print(c);
+        if (c == '\n') incRowCount();
+    }
+
+    @Override
+    public void print(char[] x) {
+        print(new String(x));
+    }
+
+    @Override
+    public void print(Object obj) {
+        print(String.valueOf(obj));
+    }
+
+    @Override
+    public PrintStream append(CharSequence csq, int start, int end) {
+        long nbNewLine = csq.chars().filter(c -> c == '\n').count();
+        incRowCount(nbNewLine);
+        return super.append(csq, start, end);
+    }
+
+    @Override
+    public PrintStream append(char c) {
+        if (c == '\n') {
+            incRowCount();
+        }
+        return super.append(c);
+    }
+
+    /* -- protected methods -- */
+
+    protected void incRowCount() {
+        synchronized (rowsCount) {
+            rowsCount++;
+        }
+    }
+
+    protected void  incRowCount(long increment) {
+        synchronized (rowsCount) {
+            rowsCount += increment;
+        }
+    }
+}
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpService.java
index a6fc7530368e6589067cca844b9608923c256c21..b1a02dee360510d05168ca6f7faa9ef50fa9893f 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpService.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpService.java
@@ -27,7 +27,10 @@ import org.duniter.core.beans.Service;
 import org.duniter.core.client.model.local.Peer;
 import org.duniter.core.client.service.exception.PeerConnectionException;
 import org.apache.http.client.methods.HttpUriRequest;
+import org.duniter.core.util.websocket.WebsocketClientEndpoint;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 
 /**
@@ -52,4 +55,12 @@ public interface HttpService extends Service {
     String getPath(String... absolutePath);
 
     URIBuilder getURIBuilder(URI baseUri, String... path);
+
+    WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path, boolean autoReconnect);
+
+    <T> T readValue(String json, Class<T> clazz) throws IOException;
+
+    <T> T readValue(byte[] json, Class<T> clazz) throws IOException;
+
+    <T> T readValue(InputStream json, Class<T> clazz) throws IOException;
 }
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 eaa8ce61df003f3f22a3b760b88d2a7c1f8e01d9..772a3de3dd8d465193f243e8dffb79e77ffb2ec2 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
@@ -45,6 +45,7 @@ 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.SimpleCache;
+import org.duniter.core.util.websocket.WebsocketClientEndpoint;
 import org.nuiton.i18n.I18n;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -53,7 +54,11 @@ import java.io.*;
 import java.net.ConnectException;
 import java.net.SocketTimeoutException;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceConfigurationError;
 
 /**
  * Created by eis on 05/02/15.
@@ -71,6 +76,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
     protected SimpleCache<Integer, RequestConfig> requestConfigCache;
     protected SimpleCache<Integer, HttpClient> httpClientCache;
 
+    protected Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>();
 
     public HttpServiceImpl() {
         super();
@@ -144,6 +150,13 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
     public void close() throws IOException {
         httpClientCache.clear();
         requestConfigCache.clear();
+
+        if (wsEndPoints.size() != 0) {
+            for (WebsocketClientEndpoint clientEndPoint: wsEndPoints.values()) {
+                clientEndPoint.close();
+            }
+            wsEndPoints.clear();
+        }
     }
 
     public <T> T executeRequest(HttpUriRequest request, Class<? extends T> resultClass)  {
@@ -359,7 +372,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
         // deserialize using gson
         else {
             try {
-                result = objectMapper.readValue(content, ResultClass);
+                result = readValue(content, ResultClass);
             }
             catch (Exception e) {
                 throw new TechnicalException(I18n.t("duniter4j.client.core.invalidResponse"), e);
@@ -430,4 +443,41 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
             // silent is gold
         }
     }
+
+    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()));
+        }
+
+    }
+
+    public <T> T readValue(String json, Class<T> clazz) throws IOException {
+        return objectMapper.readValue(json, clazz);
+    }
+
+    public <T> T readValue(byte[] json, Class<T> clazz) throws IOException {
+        return objectMapper.readValue(json, clazz);
+    }
+
+    public <T> T readValue(InputStream json, Class<T> clazz) throws IOException {
+        return objectMapper.readValue(json, clazz);
+    }
 }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BaseRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BaseRemoteServiceImpl.java
index d0d2e07375440ac61c5572d571c8011621d5dc8e..b655d2ee0ebf17e4b29709bf22ceb80d8987427a 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BaseRemoteServiceImpl.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BaseRemoteServiceImpl.java
@@ -31,11 +31,13 @@ import org.duniter.core.client.service.local.PeerService;
 import org.duniter.core.client.service.ServiceLocator;
 import org.apache.http.client.methods.HttpUriRequest;
 import org.duniter.core.exception.TechnicalException;
+import org.duniter.core.util.websocket.WebsocketClientEndpoint;
 
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.ServiceConfigurationError;
 
 /**
  * Created by eis on 05/02/15.
@@ -91,4 +93,16 @@ public abstract class BaseRemoteServiceImpl implements Service, InitializingBean
             throw new TechnicalException(e);
         }
     }
+
+    public WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path, boolean autoReconnect) {
+        return httpService.getWebsocketClientEndpoint(peer, path, autoReconnect);
+    }
+
+    public <T> T readValue(String json, Class<T> clazz) throws IOException {
+        return httpService.readValue(json, clazz);
+    }
+
+    public <T> T readValue(byte[] json, Class<T> clazz) throws IOException {
+        return httpService.readValue(json, clazz);
+    }
 }
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 8e92845eb8810efaeea6b24bc6570f56061d5db0..2231f3f2561fedfda4a6248b23d21254e4a8b521 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
@@ -226,20 +226,12 @@ public interface BlockchainRemoteService extends Service {
      * Listening new block event
      * @param currencyId
      * @param listener
+     * @param autoReconnect
      * @return
      */
-    WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener);
+    WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect);
 
-    WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener);
+    WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect);
 
-    /**
-     * 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 25c8b7a16c4cd5abab17afa43401eec92a69a157..22c7f92e4ac677b1a4fbd763a4480cf44bd87600 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
@@ -566,17 +566,17 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
     }
 
     @Override
-    public WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener) {
-        return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener);
+    public WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) {
+        return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener, autoReconnect);
     }
 
     @Override
-    public WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener) {
+    public WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) {
         Preconditions.checkNotNull(peer);
         Preconditions.checkNotNull(listener);
 
         // Get (or create) the websocket endpoint
-        WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_BLOCK);
+        WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_BLOCK, autoReconnect);
 
         // add listener
         wsClientEndPoint.registerListener(listener);
@@ -584,24 +584,6 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
         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);
-
-        return wsClientEndPoint;
-    }
 
     /* -- Internal methods -- */
 
@@ -820,29 +802,4 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
 
         return Long.parseLong(dividendStr);
     }
-
-    public WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path) {
-
-        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);
-                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/NetworkRemoteService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java
index 262dd501a4848d698d5a1be067a8bc2468cf0a1a..b8e1a0fc784258a9ee22d061019043fc4723cce2 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
@@ -46,5 +46,7 @@ public interface NetworkRemoteService extends Service {
 
     List<Peer> findPeers(Peer peer, String status, EndpointApi endpointApi, Integer currentBlockNumber, String currentBlockHash);
 
+    WebsocketClientEndpoint addPeerListener(long currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect);
+
     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 80a3c4256af7d2272f03e8f8ffdb4960656326d6..5d8b1aef38e1b490ee3209656c8353f6e78f1a3b 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
@@ -64,7 +64,6 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N
 
     protected ObjectMapper objectMapper;
 
-    private Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>();
 
     public NetworkRemoteServiceImpl() {
         super();
@@ -77,18 +76,6 @@ 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);
@@ -164,6 +151,13 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N
         return result;
     }
 
+
+    @Override
+    public WebsocketClientEndpoint addPeerListener(long currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) {
+        Peer peer = peerService.getActivePeerByCurrencyId(currencyId);
+        return addPeerListener(peer, listener, autoReconnect);
+    }
+
     @Override
     public WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) {
         Preconditions.checkNotNull(peer);
@@ -214,28 +208,5 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N
         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/local/NetworkService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java
index 0cc7f547f6c694039bb59b01b950c484edfc2345..f084cfd4aca7c690f92cef452b45b3d99b89a973 100644
--- 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
@@ -37,6 +37,10 @@ import java.util.function.Predicate;
  */
 public interface NetworkService extends Service {
 
+    interface PeersChangeListener {
+        void onChanged(List<Peer> peers);
+    }
+
     class Sort {
         public SortType sortType;
         public boolean sortAsc;
@@ -75,5 +79,11 @@ public interface NetworkService extends Service {
 
     Comparator<Peer> peerComparator(Sort sort);
 
+    void addPeersChangeListener(final Peer mainPeer, final PeersChangeListener listener);
+
+    void addPeersChangeListener(final Peer mainPeer, final PeersChangeListener listener,
+                                final Filter filter, final Sort sort, final boolean autoreconnect,
+                                final ExecutorService executor);
+
 
 }
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
index 2834bb298e43936e11df39de184d47c51f995477..7aacd7313a7f0c7ec208c0aea3b1ef5af040c231 100644
--- 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
@@ -23,14 +23,14 @@ package org.duniter.core.client.service.local;
  */
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 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.bma.*;
+import org.duniter.core.client.model.bma.jackson.JacksonUtils;
 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.BlockchainRemoteService;
 import org.duniter.core.client.service.bma.NetworkRemoteService;
 import org.duniter.core.client.service.bma.WotRemoteService;
 import org.duniter.core.client.service.exception.HttpConnectException;
@@ -45,6 +45,7 @@ import org.duniter.core.util.websocket.WebsocketClientEndpoint;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.function.Function;
@@ -65,17 +66,35 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network
     private NetworkRemoteService networkRemoteService;
     private CryptoService cryptoService;
     private WotRemoteService wotRemoteService;
+    private BlockchainRemoteService blockchainRemoteService;
+
+    protected class Locker {
+        private boolean lock = false;
+        public boolean isLocked() {
+            return this.lock;
+        }
+        public void lock() {
+            Preconditions.checkArgument(!this.lock);
+            this.lock = !this.lock;
+        }
+        public void unlock() {
+            Preconditions.checkArgument(this.lock);
+            this.lock = !this.lock;
+        }
+    }
 
     public NetworkServiceImpl() {
     }
 
     public NetworkServiceImpl(NetworkRemoteService networkRemoteService,
                               WotRemoteService wotRemoteService,
-                              CryptoService cryptoService) {
+                              CryptoService cryptoService,
+                              BlockchainRemoteService blockchainRemoteService) {
         this();
         this.networkRemoteService = networkRemoteService;
         this.wotRemoteService = wotRemoteService;
         this.cryptoService = cryptoService;
+        this.blockchainRemoteService = blockchainRemoteService;
     }
 
     @Override
@@ -84,6 +103,7 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network
         this.networkRemoteService = ServiceLocator.instance().getNetworkRemoteService();
         this.wotRemoteService = ServiceLocator.instance().getWotRemoteService();
         this.cryptoService = ServiceLocator.instance().getCryptoService();
+        this.blockchainRemoteService = ServiceLocator.instance().getBlockchainRemoteService();
     }
 
     @Override
@@ -146,37 +166,41 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network
                 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());
+                    return peersFuture.join().stream()
+                            .map(peer -> asyncRefreshPeer(peer, memberUids, pool))
+                            .collect(Collectors.toList());
+                });
+    }
+
+    public CompletableFuture<Peer> asyncRefreshPeer(final Peer peer, final Map<String, String> memberUids, final ExecutorService pool) {
+        return 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;
                 });
     }
 
@@ -223,6 +247,135 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network
         return peers;
     }
 
+    public void addPeersChangeListener(final Peer mainPeer, final PeersChangeListener listener) {
+
+        // 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;
+
+        addPeersChangeListener(mainPeer, listener, filterDef, sortDef, true, null);
+
+    }
+
+    public void addPeersChangeListener(final Peer mainPeer, final PeersChangeListener listener,
+                                       final Filter filter, final Sort sort, final boolean autoreconnect,
+                                       final ExecutorService executor) {
+
+
+        final  Locker threadLock = new Locker();
+        final List<Peer> result = new ArrayList<>();
+        final List<String> knownBlocks = new ArrayList<>();
+        final Map<String, Peer> knownPeers = new HashMap<>();
+
+        final Predicate<Peer> peerFilter = peerFilter(filter);
+        final Comparator<Peer> peerComparator = peerComparator(sort);
+        final ExecutorService pool = (executor != null) ? executor : ForkJoinPool.commonPool();
+
+        // Manage new block event
+        blockchainRemoteService.addBlockListener(mainPeer, json -> {
+            if (threadLock.isLocked()) return;
+            synchronized (threadLock) {
+                threadLock.lock();
+            }
+            try {
+                BlockchainBlock block = readValue(json, BlockchainBlock.class);
+                String blockBuid = buid(block);
+                boolean isNewBlock = (blockBuid != null && !knownBlocks.contains(blockBuid));
+
+                // If new block + wait 5s for network propagation
+                if (isNewBlock && waitSafe(5000)) {
+                    List<Peer> updatedPeers = getPeers(mainPeer, filter, sort);
+
+                    knownPeers.clear();
+                    updatedPeers.stream().forEach(peer -> {
+                        String buid = buid(peer.getStats());
+                        if (!knownBlocks.contains(buid)) {
+                            knownBlocks.add(buid);
+                        }
+                        knownPeers.put(peer.toString(), peer);
+                    });
+
+                    result.clear();
+                    result.addAll(updatedPeers);
+                    listener.onChanged(result);
+                }
+            } catch(IOException e) {
+                log.error("Could not parse peer received by WS: " + e.getMessage(), e);
+            }
+            finally {
+                synchronized (threadLock) {
+                    threadLock.unlock();
+                }
+            }
+        }, autoreconnect);
+
+        // Manage new peer event
+        networkRemoteService.addPeerListener(mainPeer, json -> {
+            if (threadLock.isLocked()) return;
+            synchronized (threadLock) {
+                threadLock.lock();
+            }
+
+            try {
+                NetworkPeers.Peer bmaPeer = readValue(json, NetworkPeers.Peer.class);
+                final List<Peer> newPeers = new ArrayList<>();
+                addEndpointsAsPeers(bmaPeer, newPeers, null);
+
+                CompletableFuture<List<CompletableFuture<Peer>>> jobs =
+                        CompletableFuture.supplyAsync(() -> wotRemoteService.getMembersUids(mainPeer), pool)
+                    .thenApply(memberUids ->
+                        newPeers.stream().map(peer ->
+                            asyncRefreshPeer(peer, memberUids, pool))
+                            .collect(Collectors.toList())
+                    );
+                jobs.thenCompose(refreshedPeersFuture -> CompletableFutures.allOfToList(refreshedPeersFuture, refreshedPeer -> {
+                        boolean exists = knownPeers.containsKey(refreshedPeer.toString());
+                        boolean include = peerFilter.test(refreshedPeer);
+                        if (!include && exists) {
+                            Peer removedPeer = knownPeers.remove(refreshedPeer.toString());
+                            result.remove(removedPeer);
+                        }
+                        else if (include && exists) {
+                            result.remove(knownPeers.get(refreshedPeer.toString()));
+                        }
+                        return include;
+                    }))
+                    .thenApply(addedPeers -> {
+                        result.addAll(addedPeers);
+                        fillPeerStatsConsensus(result);
+                        result.sort(peerComparator);
+
+                        result.stream().forEach(peer -> {
+                            String buid = buid(peer.getStats());
+                            if (!knownBlocks.contains(buid)) {
+                                knownBlocks.add(buid);
+                            }
+                            knownPeers.put(peer.toString(), peer);
+                        });
+
+                        listener.onChanged(result);
+                        return result;
+                    });
+
+            } catch(IOException e) {
+                log.error("Could not parse peer received by WS: " + e.getMessage(), e);
+            }
+            finally {
+                synchronized (threadLock) {
+                    threadLock.unlock();
+                }
+            }
+
+        }, autoreconnect);
+    }
+
+
     /* -- protected methods -- */
 
     protected List<Peer> loadPeerLeafs(Peer peer) {
@@ -266,21 +419,7 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network
             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);
-                        }
-                    }
-                }
-
+                addEndpointsAsPeers(peer, result, leaf);
 
             } catch(HttpNotFoundException e) {
                 log.warn("Peer not found for leaf=" + leaf);
@@ -289,6 +428,22 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network
         }
     }
 
+    protected void addEndpointsAsPeers(NetworkPeers.Peer peer, List<Peer> result, String hash) {
+        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(hash)
+                            .setPubkey(peer.getPubkey())
+                            .setEndpoint(ep)
+                            .build();
+                    result.add(peerEp);
+                }
+            }
+        }
+    }
+
 
     protected boolean applyPeerFilter(Peer peer, Filter filter) {
 
@@ -463,4 +618,18 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network
                 ? stats.getBlockNumber() + "-" + stats.getBlockHash()
                 : null;
     }
+
+    protected String buid(BlockchainBlock block) {
+        if (block == null || block.getNumber() == null || block.getHash() == null) return null;
+        return block.getNumber() + "-" + block.getHash();
+    }
+
+    protected boolean waitSafe(long duration) {
+        try {
+            Thread.sleep(duration);
+            return true;
+        } catch (InterruptedException e) {
+            return false;
+        }
+    }
 }
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 33ee0035fd0ca25d5d2ea0b87c946949590222cd..855fad2054b24ce985afa3a9c0a8181d83ec0765 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
@@ -146,7 +146,7 @@ public class BlockchainRemoteServiceTest {
 
         isWebSocketNewBlockReceived = false;
 
-        service.addBlockListener(createTestPeer(), (message) -> {
+        /*service.addBlockListener(createTestPeer(), (message) -> {
             try {
                 BlockchainBlock block = JacksonUtils.newObjectMapper().readValue(message, BlockchainBlock.class);
                 log.debug("Received block #" + block.getNumber());
@@ -155,7 +155,7 @@ public class BlockchainRemoteServiceTest {
             catch (IOException e) {
                 Assert.fail(e.getMessage());
             }
-        });
+        });*/
 
         int count = 0;
         while(!isWebSocketNewBlockReceived) {