From 3d77780200930a0ce4c54e425238147e3b59d7f9 Mon Sep 17 00:00:00 2001
From: blavenie <benoit.lavenier@e-is.pro>
Date: Thu, 8 Dec 2016 19:29:10 +0100
Subject: [PATCH] - Use Jackson for JSON blockchain deserialization - Add user
 event on blockchain changes

---
 LICENSE                                       |   2 +-
 duniter4j-core-client/LICENSE                 |   2 +-
 duniter4j-core-client/pom.xml                 |  80 +++----
 .../client/model/bma/BlockchainBlock.java     | 154 +++++++++++--
 .../duniter/core/client/model/bma/Blocks.java |  54 +++++
 .../core/client/model/bma/NetworkPeering.java |  63 +++++-
 .../core/client/model/bma/NetworkPeers.java   |  92 ++++++++
 .../core/client/model/bma/Protocol.java       |   2 +
 .../core/client/model/bma/TxSource.java       |   9 +-
 .../client/model/bma/WotCertification.java    |  61 +++++-
 .../core/client/model/bma/WotLookup.java      | 149 ++++++++++---
 .../model/bma/gson/JoinerTypeAdapter.java     |   8 +-
 .../model/bma/gson/RevokedTypeAdapter.java    |   2 +-
 .../bma/jackson/EndpointDeserializer.java     |  51 +++++
 .../bma/jackson/IdentityDeserializer.java     |  40 ++++
 .../model/bma/jackson/JacksonUtils.java       |  27 +++
 .../model/bma/jackson/JoinerDeserializer.java |  40 ++++
 .../bma/jackson/RevokedDeserializer.java      |  36 +++
 .../client/model/elasticsearch/Record.java    |   6 +-
 .../core/client/service/HttpServiceImpl.java  |  42 ++--
 .../service/bma/BlockchainRemoteService.java  |  12 +-
 .../bma/BlockchainRemoteServiceImpl.java      |  24 +-
 .../bma/TransactionRemoteServiceImpl.java     | 206 ++++++++++++++----
 .../service/bma/WotRemoteServiceImpl.java     |  18 +-
 .../CurrencyRegistryRemoteServiceImpl.java    |   3 +-
 .../duniter4j-core-client_fr_FR.properties    |   6 +-
 .../core/client/service/HttpServiceTest.java  |  73 +++++++
 .../bma/BlockchainRemoteServiceTest.java      |  30 ++-
 .../bma/TransactionRemoteServiceTest.java     |   2 -
 .../service/bma/WotRemoteServiceTest.java     |   4 +-
 .../duniter4j-core-client-test.properties     |   8 +-
 .../src/test/resources/log4j.properties       |   2 +-
 duniter4j-core-shared/LICENSE                 |   2 +-
 duniter4j-es-core/LICENSE                     |   2 +-
 .../duniter/elasticsearch/PluginSettings.java |   4 +
 .../service/CurrencyService.java              |   2 +-
 .../service/changes/ChangeEvent.java          |  58 +++++
 .../service/changes/ChangeListener.java       |   6 -
 .../service/changes/ChangeService.java        | 130 ++++++-----
 .../service/changes/ChangeSource.java         |  61 ++++++
 .../service/changes/ChangeUtils.java          |  65 ------
 ...int.java => WebSocketChangesEndPoint.java} |  33 ++-
 .../websocket/WebSocketModule.java            |   3 +-
 .../websocket/WebSocketServer.java            |   1 -
 .../gchange/service/MarketService.java        |   2 +-
 .../elasticsearch/user/model/UserEvent.java   | 105 +++++++--
 .../user/model/UserEventCodes.java            |  11 +-
 .../user/model/UserEventLink.java             |  74 -------
 .../service/BlockchainUserEventService.java   | 190 ++++++++++++++++
 .../user/service/ServiceModule.java           |   5 +-
 .../user/service/UserEventService.java        |  92 +++++---
 .../user/service/UserService.java             |   6 +-
 .../i18n/duniter4j-es-user_en_GB.properties   |  11 +-
 .../i18n/duniter4j-es-user_fr_FR.properties   |   8 +-
 54 files changed, 1680 insertions(+), 499 deletions(-)
 create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Blocks.java
 create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java
 create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/IdentityDeserializer.java
 create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JacksonUtils.java
 create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JoinerDeserializer.java
 create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/RevokedDeserializer.java
 create mode 100644 duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java
 delete mode 100644 duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeListener.java
 delete mode 100644 duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeUtils.java
 rename duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/{changes/WebSocketChangeEndPoint.java => WebSocketChangesEndPoint.java} (70%)
 delete mode 100644 duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventLink.java
 create mode 100644 duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/BlockchainUserEventService.java

diff --git a/LICENSE b/LICENSE
index 94a9ed02..6df63bc4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -552,7 +552,7 @@ License would be to refrain entirely from conveying the Program.
   13. Use with the GNU Affero General Public License.
 
   Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
+permission to reference or combine any covered work with a work licensed
 under version 3 of the GNU Affero General Public License into a single
 combined work, and to convey the resulting work.  The terms of this
 License will continue to apply to the part which is the covered work,
diff --git a/duniter4j-core-client/LICENSE b/duniter4j-core-client/LICENSE
index 94a9ed02..6df63bc4 100644
--- a/duniter4j-core-client/LICENSE
+++ b/duniter4j-core-client/LICENSE
@@ -552,7 +552,7 @@ License would be to refrain entirely from conveying the Program.
   13. Use with the GNU Affero General Public License.
 
   Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
+permission to reference or combine any covered work with a work licensed
 under version 3 of the GNU Affero General Public License into a single
 combined work, and to convey the resulting work.  The terms of this
 License will continue to apply to the part which is the covered work,
diff --git a/duniter4j-core-client/pom.xml b/duniter4j-core-client/pom.xml
index 4cdc3b20..d71653b9 100644
--- a/duniter4j-core-client/pom.xml
+++ b/duniter4j-core-client/pom.xml
@@ -39,50 +39,54 @@
       <scope>runtime</scope>
     </dependency>
 
-      <dependency>
-          <groupId>org.glassfish.tyrus</groupId>
-          <artifactId>tyrus-client</artifactId>
-          <version>1.12</version>
-      </dependency>
+    <dependency>
+        <groupId>org.glassfish.tyrus</groupId>
+        <artifactId>tyrus-client</artifactId>
+        <version>1.12</version>
+    </dependency>
 
-      <dependency>
-          <groupId>org.glassfish.tyrus</groupId>
-          <artifactId>tyrus-container-grizzly-client</artifactId>
-          <version>1.12</version>
-      </dependency>
+    <dependency>
+        <groupId>org.glassfish.tyrus</groupId>
+        <artifactId>tyrus-container-grizzly-client</artifactId>
+        <version>1.12</version>
+    </dependency>
 
-      <dependency>
-          <groupId>org.apache.commons</groupId>
-          <artifactId>commons-lang3</artifactId>
-      </dependency>
-      <dependency>
-          <groupId>org.apache.commons</groupId>
-          <artifactId>commons-collections4</artifactId>
-      </dependency>
-      <dependency>
-          <groupId>org.apache.httpcomponents</groupId>
-          <artifactId>httpclient</artifactId>
-      </dependency>
-      <dependency>
-          <groupId>org.apache.httpcomponents</groupId>
-          <artifactId>httpcore</artifactId>
-      </dependency>
-      <dependency>
-          <groupId>org.apache.httpcomponents</groupId>
-          <artifactId>httpmime</artifactId>
-      </dependency>
-      <dependency>
-          <groupId>org.nuiton</groupId>
-          <artifactId>nuiton-config</artifactId>
-      </dependency>
-      <dependency>
-          <groupId>org.nuiton.i18n</groupId>
-          <artifactId>nuiton-i18n</artifactId>
-      </dependency>
+    <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-lang3</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-collections4</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.apache.httpcomponents</groupId>
+        <artifactId>httpclient</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.apache.httpcomponents</groupId>
+        <artifactId>httpcore</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.apache.httpcomponents</groupId>
+        <artifactId>httpmime</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.nuiton</groupId>
+        <artifactId>nuiton-config</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.nuiton.i18n</groupId>
+        <artifactId>nuiton-i18n</artifactId>
+    </dependency>
     <dependency>
       <groupId>com.google.code.gson</groupId>
       <artifactId>gson</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+    </dependency>
 
     <!-- Unit test -->
     <dependency>
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/BlockchainBlock.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/BlockchainBlock.java
index 2da348bd..74f7c52c 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/BlockchainBlock.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/BlockchainBlock.java
@@ -24,6 +24,12 @@ package org.duniter.core.client.model.bma;
 
 
 
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
 import java.io.Serializable;
 import java.math.BigInteger;
 
@@ -45,14 +51,17 @@ public class BlockchainBlock implements Serializable {
     private Long medianTime;
     private Integer membersCount;
     private BigInteger monetaryMass;
-    private Integer unitBase;
+    private Integer unitbase;
+    private Integer issuersCount;
+    private Integer issuersFrame;
+    private Integer issuersFrameVar;
     private String currency;
     private String issuer;
     private String hash;
     private String parameters;
     private String previousHash;
     private String previousIssuer;
-    private String inner_hash;
+    private String innerHash;
     private BigInteger dividend;
     private Identity[] identities;
     private Joiner[] joiners;
@@ -66,7 +75,7 @@ public class BlockchainBlock implements Serializable {
 
 
 //  raw": "Version: 1\nType: Block\nCurrency: zeta_brouzouf\nNonce: 8233\nNumber: 1\nDate: 1416589860\nConfirmedDate: 1416589860\nIssuer: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\nPreviousHash: 00006CD96A01378465318E48310118AC6B2F3625\nPreviousIssuer: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\nMembersCount: 4\nIdentities:\nJoiners:\nActives:\nLeavers:\nExcluded:\nCertifications:\nTransactions:\n"
-    //private String raw;
+    private String raw;
 
     public String getVersion() {
         return version;
@@ -183,20 +192,22 @@ public class BlockchainBlock implements Serializable {
         this.joiners = joiners;
     }
 
-    public Integer getUnitBase() {
-        return unitBase;
+    public Integer getUnitbase() {
+        return unitbase;
     }
 
-    public void setUnitBase(Integer unitBase) {
-        this.unitBase = unitBase;
+    public void setUnitbase(Integer unitbase) {
+        this.unitbase = unitbase;
     }
 
+    @JsonGetter("inner_hash")
     public String getInnerHash() {
-        return inner_hash;
+        return innerHash;
     }
 
+    @JsonSetter("inner_hash")
     public void setInnerHash(String inner_hash) {
-        this.inner_hash = inner_hash;
+        this.innerHash = inner_hash;
     }
 
     public Joiner[] getLeavers() {
@@ -247,10 +258,42 @@ public class BlockchainBlock implements Serializable {
         this.transactions = transactions;
     }
 
+    public Integer getIssuersCount() {
+        return issuersCount;
+    }
+
+    public void setIssuersCount(Integer issuersCount) {
+        this.issuersCount = issuersCount;
+    }
+
+    public Integer getIssuersFrame() {
+        return issuersFrame;
+    }
+
+    public void setIssuersFrame(Integer issuersFrame) {
+        this.issuersFrame = issuersFrame;
+    }
+
+    public Integer getIssuersFrameVar() {
+        return issuersFrameVar;
+    }
+
+    public void setIssuersFrameVar(Integer issuersFrameVar) {
+        this.issuersFrameVar = issuersFrameVar;
+    }
+
+    public String getRaw() {
+        return raw;
+    }
+
+    public void setRaw(String raw) {
+        this.raw = raw;
+    }
+
     public String toString() {
         String s = "version=" + version;
         s += "\nnonce=" + nonce;
-        s += "\ninner_hash=" + inner_hash;
+        s += "\ninnerHash=" + innerHash;
         s += "\nnumber=" + number;
         s += "\npowMin" + powMin;
         s += "\ntime=" + time;
@@ -312,6 +355,7 @@ public class BlockchainBlock implements Serializable {
         return s;
     }
 
+    @JsonDeserialize
     public static class Identity implements Serializable {
 
         private static final long serialVersionUID = 8080689271400316984L;
@@ -379,9 +423,9 @@ public class BlockchainBlock implements Serializable {
 
         private String userId;
 
-        private String mBlockUid;
+        private String membershipBlockUid;
 
-        private String iBlockUid;
+        private String idtyBlockUid;
 
         public String getPublicKey() {
             return publicKey;
@@ -407,20 +451,20 @@ public class BlockchainBlock implements Serializable {
             this.userId = uid;
         }
 
-        public String getMBlockUid() {
-            return mBlockUid;
+        public String getMembershipBlockUid() {
+            return membershipBlockUid;
         }
 
-        public void setMBlockUid(String mBlockUid) {
-            this.mBlockUid = mBlockUid;
+        public void setMembershipBlockUid(String membershipBlockUid) {
+            this.membershipBlockUid = membershipBlockUid;
         }
 
-        public String getIBlockUid() {
-            return iBlockUid;
+        public String getIdtyBlockUid() {
+            return idtyBlockUid;
         }
 
-        public void setIBlockUid(String iBlockUid) {
-            this.iBlockUid = iBlockUid;
+        public void setIdtyBlockUid(String idtyBlockUid) {
+            this.idtyBlockUid = idtyBlockUid;
         }
 
         @Override
@@ -429,8 +473,8 @@ public class BlockchainBlock implements Serializable {
             StringBuilder sb = new StringBuilder()
                     .append(":").append(publicKey)
                     .append(":").append(signature)
-                    .append(":").append(mBlockUid)
-                    .append(":").append(iBlockUid)
+                    .append(":").append(membershipBlockUid)
+                    .append(":").append(idtyBlockUid)
                     .append(":").append(userId);
 
             return sb.toString();
@@ -468,7 +512,9 @@ public class BlockchainBlock implements Serializable {
         }
     }
 
-    public class Transaction implements Serializable {
+
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Transaction implements Serializable {
         private static final long serialVersionUID = 1L;
 
         private String[] signatures;
@@ -485,6 +531,18 @@ public class BlockchainBlock implements Serializable {
 
         private String[] outputs;
 
+        private long time;
+
+        private long locktime;
+
+        private String blockstamp;
+
+        private long blockstampTime;
+
+        private String comment;
+
+        private long blockNumber;
+
         public String[] getSignatures() {
             return signatures;
         }
@@ -541,6 +599,56 @@ public class BlockchainBlock implements Serializable {
             this.outputs = outputs;
         }
 
+        public long getTime() {
+            return time;
+        }
+
+        public void setTime(long time) {
+            this.time = time;
+        }
+
+        public long getBlockstampTime() {
+            return blockstampTime;
+        }
+
+        public void setBlockstampTime(long blockstampTime) {
+            this.blockstampTime = blockstampTime;
+        }
+
+        public long getLocktime() {
+            return locktime;
+        }
+
+        public void setLocktime(long locktime) {
+            this.locktime = locktime;
+        }
+
+        public String getBlockstamp() {
+            return blockstamp;
+        }
+
+        public void setBlockstamp(String blockstamp) {
+            this.blockstamp = blockstamp;
+        }
+
+        public String getComment() {
+            return comment;
+        }
+
+        public void setComment(String comment) {
+            this.comment = comment;
+        }
+
+        @JsonGetter("block_number")
+        public long getBlockNumber() {
+            return blockNumber;
+        }
+
+        @JsonSetter("block_number")
+        public void setBlockNumber(long blockNumber) {
+            this.blockNumber = blockNumber;
+        }
+
         @Override
         public String toString() {
             StringBuilder sb = new StringBuilder();
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Blocks.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Blocks.java
new file mode 100644
index 00000000..25e4874f
--- /dev/null
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Blocks.java
@@ -0,0 +1,54 @@
+package org.duniter.core.client.model.bma;
+
+/*
+ * #%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 java.io.Serializable;
+
+public class Blocks {
+
+    private Result result;
+
+    public Result getResult() {
+        return result;
+    }
+
+    public void setResult(Result result) {
+        this.result = result;
+    }
+
+    public static class Result implements Serializable {
+
+        private static final long serialVersionUID = -39452685440482106L;
+
+        private long[] blocks;
+
+        public long[] getBlocks() {
+            return blocks;
+        }
+
+        public void setBlocks(long[] blocks) {
+            this.blocks = blocks;
+        }
+    }
+}
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 3fc7e3e9..1d24f9c5 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
@@ -22,6 +22,8 @@ package org.duniter.core.client.model.bma;
  * #L%
  */
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 import java.io.Serializable;
 
 /**
@@ -32,7 +34,9 @@ public class NetworkPeering implements Serializable {
     private String currency;
     private String block;
     private String signature;
-    // not need : private String raw
+
+    private String raw;
+
     private String pubkey;
 
     public Endpoint[] endpoints;
@@ -77,6 +81,23 @@ public class NetworkPeering implements Serializable {
         this.pubkey = pubkey;
     }
 
+    @JsonIgnore
+    public String getRaw() {
+        return raw;
+    }
+
+    public void setRaw(String raw) {
+        this.raw = raw;
+    }
+
+    public Endpoint[] getEndpoints() {
+        return endpoints;
+    }
+
+    public void setEndpoints(Endpoint[] endpoints) {
+        this.endpoints = endpoints;
+    }
+
     public String toString() {
         String s = "version=" + version + "\n" +
                 "currency=" + currency + "\n" +
@@ -97,6 +118,46 @@ public class NetworkPeering implements Serializable {
         public String ipv6;
         public Integer port;
 
+        public EndpointProtocol getProtocol() {
+            return protocol;
+        }
+
+        public void setProtocol(EndpointProtocol protocol) {
+            this.protocol = protocol;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+
+        public void setUrl(String url) {
+            this.url = url;
+        }
+
+        public String getIpv4() {
+            return ipv4;
+        }
+
+        public void setIpv4(String ipv4) {
+            this.ipv4 = ipv4;
+        }
+
+        public String getIpv6() {
+            return ipv6;
+        }
+
+        public void setIpv6(String ipv6) {
+            this.ipv6 = ipv6;
+        }
+
+        public Integer getPort() {
+            return port;
+        }
+
+        public void setPort(Integer port) {
+            this.port = port;
+        }
+
         @Override
         public String toString() {
             String s = "protocol=" + protocol.name() + "\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 76f11af0..90cbf2cf 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
@@ -22,6 +22,10 @@ package org.duniter.core.client.model.bma;
  * #L%
  */
 
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonSetter;
+
 import java.io.Serializable;
 
 /**
@@ -46,8 +50,96 @@ public class NetworkPeers implements Serializable {
         public String block;
         public String signature;
         public String pubkey;
+        public Long firstDown;
+        public Long lastTry;
+        public String raw;
         public NetworkPeering.Endpoint[] endpoints;
 
+        public String getVersion() {
+            return version;
+        }
+
+        public void setVersion(String version) {
+            this.version = version;
+        }
+
+        public String getCurrency() {
+            return currency;
+        }
+
+        public void setCurrency(String currency) {
+            this.currency = currency;
+        }
+
+        public String getStatus() {
+            return status;
+        }
+
+        public void setStatus(String status) {
+            this.status = status;
+        }
+
+        public String getBlock() {
+            return block;
+        }
+
+        public void setBlock(String block) {
+            this.block = block;
+        }
+
+        public String getSignature() {
+            return signature;
+        }
+
+        public void setSignature(String signature) {
+            this.signature = signature;
+        }
+
+        public String getPubkey() {
+            return pubkey;
+        }
+
+        public void setPubkey(String pubkey) {
+            this.pubkey = pubkey;
+        }
+
+        @JsonGetter("first_down")
+        public Long getFirstDown() {
+            return firstDown;
+        }
+
+        @JsonSetter("first_down")
+        public void setFirstDown(Long firstDown) {
+            this.firstDown = firstDown;
+        }
+
+        @JsonGetter("last_try")
+        public Long getLastTry() {
+            return lastTry;
+        }
+
+        @JsonSetter("last_try")
+        public void setLastTry(Long lastTry) {
+            this.lastTry = lastTry;
+        }
+
+        public NetworkPeering.Endpoint[] getEndpoints() {
+            return endpoints;
+        }
+
+        public void setEndpoints(NetworkPeering.Endpoint[] endpoints) {
+            this.endpoints = endpoints;
+        }
+
+        @JsonIgnore
+        public String getRaw() {
+            return raw;
+        }
+
+        public void setRaw(String raw) {
+            this.raw = raw;
+        }
+
         @Override
         public String toString() {
             String s = "version=" + version + "\n" +
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 016b7edc..20653d2d 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
@@ -29,6 +29,8 @@ public interface Protocol {
 
     String VERSION = "2";
 
+    String TX_VERSION = "3";
+
     String TYPE_IDENTITY = "Identity";
 
     String TYPE_MEMBERSHIP = "Membership";
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 e1eaf7ef..36e4901a 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
@@ -23,6 +23,8 @@ package org.duniter.core.client.model.bma;
  */
 
 
+import com.fasterxml.jackson.annotation.JsonCreator;
+
 import java.io.Serializable;
 import java.util.List;
 
@@ -34,6 +36,9 @@ public class TxSource {
 	    
     private Source[] sources;
 
+	public TxSource() {
+	}
+
 	public String getCurrency() {
 		return currency;
 	}
@@ -58,7 +63,7 @@ public class TxSource {
 		this.sources = sources;
 	}
 
-	public class Source implements Serializable, Cloneable {
+	public static class Source implements Serializable, Cloneable {
 
 		private static final long serialVersionUID = 8084087351543574142L;
 
@@ -68,6 +73,8 @@ public class TxSource {
 		private long amount;
 		private int base;
 
+		public Source() {
+		}
 
 		@Override
 		public Object clone() throws CloneNotSupportedException {
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/WotCertification.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/WotCertification.java
index ea0c259c..6ded2537 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/WotCertification.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/WotCertification.java
@@ -23,6 +23,8 @@ package org.duniter.core.client.model.bma;
  */
 
 
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonSetter;
 import org.duniter.core.client.model.local.Identity;
 
 import java.io.Serializable;
@@ -41,6 +43,10 @@ public class WotCertification implements Serializable{
 
     private String uid;
 
+    private String sigDate;
+
+    private boolean isMember;
+
     private Certification[] certifications;
 
     public Certification[] getCertifications() {
@@ -67,23 +73,46 @@ public class WotCertification implements Serializable{
         this.uid = uid;
     }
 
-    public class Certification extends Identity {
+    @JsonGetter("is_member")
+    public boolean isMember() {
+        return isMember;
+    }
+
+    public void setIsMember(boolean isMember) {
+        this.isMember = isMember;
+    }
+
+    public String getSigDate() {
+        return sigDate;
+    }
+
+    public void setSigDate(String sigDate) {
+        this.sigDate = sigDate;
+    }
+
+    public static class Certification extends Identity {
 
         private static final long serialVersionUID = 2204517069552693026L;
 
-        public CertTime cert_time;
+        private CertTime certTime;
+
+        private String sigDate;
 
         /**
          * Indicate whether the certification is written in the blockchain or not.
          */
         private Written written;
 
-        public CertTime getCert_time() {
-            return cert_time;
+        private boolean wasMember;
+
+        @JsonGetter("cert_time")
+        public CertTime getCertTime() {
+            return certTime;
         }
 
-        public void setCert_time(CertTime cert_time) {
-            this.cert_time = cert_time;
+        @JsonSetter("cert_time")
+        public void setCertTime(CertTime certTime) {
+            this.certTime = certTime;
         }
 
         /**
@@ -97,9 +126,25 @@ public class WotCertification implements Serializable{
             this.written = written;
         }
 
+        public String getSigDate() {
+            return sigDate;
+        }
+
+        public void setSigDate(String sigDate) {
+            this.sigDate = sigDate;
+        }
+
+        @JsonGetter("wasMember")
+        public boolean wasMember() {
+            return wasMember;
+        }
+
+        public void setWasMember(boolean wasMember) {
+            this.wasMember = wasMember;
+        }
     }
 
-    public class CertTime implements Serializable {
+    public static class CertTime implements Serializable {
 
         private static final long serialVersionUID = -358639516878884523L;
 
@@ -125,7 +170,7 @@ public class WotCertification implements Serializable{
 
     }
 
-    public class Written implements Serializable{
+    public static class Written implements Serializable{
 
         private long number = -1;
 
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/WotLookup.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/WotLookup.java
index 8772e350..691a1b28 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/WotLookup.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/WotLookup.java
@@ -23,12 +23,15 @@ package org.duniter.core.client.model.bma;
  */
 
 
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonSetter;
+
 import java.io.Serializable;
 
 public class WotLookup {
 
-    public boolean partial;
-    public Result[] results;
+    private boolean partial;
+    private Result[] results;
 
     public boolean isPartial() {
         return partial;
@@ -63,9 +66,9 @@ public class WotLookup {
 
         private static final long serialVersionUID = -39452685440482106L;
 
-        public String pubkey;
-        public Uid[] uids;
-        public SignedSignature[] signed;
+        private String pubkey;
+        private Uid[] uids;
+        private SignedSignature[] signed;
 
         public String getPubkey() {
             return pubkey;
@@ -92,14 +95,19 @@ public class WotLookup {
         }
     }
 
-    public class Uid {
+    public static class Uid {
+
+        private String uid;
+        private Meta meta;
+        private String self;
+        private Boolean revoked;
+        private Long revokedOn;
+        private String revocationSig;
+        private OtherSignature[] others;
 
-        public String uid;
-        public Meta meta;
-        public String self;
-        public Boolean revoked;
-        public String revocation_sig;
-        public OtherSignature[] others;
+        public Uid(){
+
+        }
 
         public String getUid() {
             return uid;
@@ -141,19 +149,32 @@ public class WotLookup {
             this.revoked = revoked;
         }
 
+        @JsonGetter("revocation_sig")
         public String getRevocationSig() {
-            return revocation_sig;
+            return revocationSig;
         }
 
+        @JsonSetter("revocation_sig")
         public void setRevocationSig(String revocationSig) {
-            this.revocation_sig = revocationSig;
+            this.revocationSig = revocationSig;
+        }
+
+        @JsonGetter("revoked_on")
+        public Long getRevokedOn() {
+            return revokedOn;
+        }
+
+        @JsonSetter("revoked_on")
+        public void setRevokedOn(Long revokedOn) {
+            this.revokedOn = revokedOn;
         }
     }
 
 
-    public class Meta implements Serializable {
-        public String timestamp;
-        public Long block_number;
+    public static class Meta implements Serializable {
+        private String timestamp;
+        private String blockHash;
+        private Long blockNumber;
 
         public String getTimestamp() {
             return timestamp;
@@ -162,16 +183,36 @@ public class WotLookup {
         public void setTimestamp(String timestamp) {
             this.timestamp = timestamp;
         }
+
+        @JsonGetter("block_hash")
+        public String getBlockHash() {
+            return blockHash;
+        }
+
+        @JsonSetter("block_hash")
+        public void setBlockHash(String blockHash) {
+            this.blockHash = blockHash;
+        }
+        @JsonGetter("block_number")
+        public Long getBlockNumber() {
+            return blockNumber;
+        }
+
+        @JsonSetter("block_number")
+        public void setBlockNumberH(Long blockNumber) {
+            this.blockNumber = blockNumber;
+        }
+
     }
 
-    public class OtherSignature {
+    public static class OtherSignature {
 
-        public String pubkey;
-        public Meta meta;
-        public String signature;
-        public String[] uids;
-        public boolean isMember;
-        public boolean wasMember;
+        private String pubkey;
+        private Meta meta;
+        private String signature;
+        private String[] uids;
+        private boolean isMember;
+        private boolean wasMember;
 
         public String getPubkey() {
             return pubkey;
@@ -205,15 +246,18 @@ public class WotLookup {
             this.uids = uids;
         }
 
+        @JsonGetter("isMember")
         public boolean isMember() {
             return isMember;
         }
 
+        @JsonSetter("isMember")
         public void setMember(boolean member) {
             isMember = member;
         }
 
-        public boolean isWasMember() {
+        @JsonGetter("wasMember")
+        public boolean wasMember() {
             return wasMember;
         }
 
@@ -222,14 +266,15 @@ public class WotLookup {
         }
     }
 
-    public class SignedSignature {
+    public static class SignedSignature {
 
-        public String uid;
-        public String pubkey;
-        public Meta meta;
-        public String signature;
-        public boolean isMember;
-        public boolean wasMember;
+        private String uid;
+        private String pubkey;
+        private Meta meta;
+        private CertTime cerTime;
+        private String signature;
+        private boolean isMember;
+        private boolean wasMember;
 
         public String getUid() {
             return uid;
@@ -263,21 +308,55 @@ public class WotLookup {
             this.signature = signature;
         }
 
+        @JsonGetter("isMember")
         public boolean isMember() {
             return isMember;
         }
 
-        public void setMember(boolean member) {
-            isMember = member;
+        public void setIsMember(boolean isMember) {
+            this.isMember = isMember;
         }
 
-        public boolean isWasMember() {
+        @JsonGetter("wasMember")
+        public boolean wasMember() {
             return wasMember;
         }
 
         public void setWasMember(boolean wasMember) {
             this.wasMember = wasMember;
         }
+
+        @JsonGetter("cert_time")
+        public CertTime getCerTime() {
+            return cerTime;
+        }
+
+        @JsonSetter("cert_time")
+        public void setCerTime(CertTime cerTime) {
+            this.cerTime = cerTime;
+        }
     }
 
+    public static class CertTime implements Serializable {
+        private Long block;
+        private String blockHash;
+
+        public Long getBlock() {
+            return block;
+        }
+
+        public void setBlock(Long block) {
+            this.block = block;
+        }
+
+        @JsonGetter("block_hash")
+        public String getBlockHash() {
+            return blockHash;
+        }
+
+        @JsonSetter("block_hash")
+        public void setBlockHash(String blockHash) {
+            this.blockHash = blockHash;
+        }
+    }
 }
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/JoinerTypeAdapter.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/JoinerTypeAdapter.java
index 33af16e3..cb574675 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/JoinerTypeAdapter.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/JoinerTypeAdapter.java
@@ -48,8 +48,8 @@ public class JoinerTypeAdapter implements JsonDeserializer<BlockchainBlock.Joine
         
         result.setPublicKey(identityParts[i++]);
         result.setSignature(identityParts[i++]);
-        result.setMBlockUid(identityParts[i++]);
-        result.setIBlockUid(identityParts[i++]);
+        result.setMembershipBlockUid(identityParts[i++]);
+        result.setIdtyBlockUid(identityParts[i++]);
         result.setUserId(identityParts[i++]);
 
         return result;
@@ -60,8 +60,8 @@ public class JoinerTypeAdapter implements JsonDeserializer<BlockchainBlock.Joine
         String result = new StringBuilder()
                 .append(member.getPublicKey()).append(":")
                 .append(member.getSignature()).append(":")
-                .append(member.getMBlockUid()).append(":")
-                .append(member.getIBlockUid()).append(":")
+                .append(member.getMembershipBlockUid()).append(":")
+                .append(member.getIdtyBlockUid()).append(":")
                 .append(member.getUserId()).toString();
 
         return context.serialize(result.toString(), String.class);
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/RevokedTypeAdapter.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/RevokedTypeAdapter.java
index 4f49b139..1948ecd8 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/RevokedTypeAdapter.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/RevokedTypeAdapter.java
@@ -39,7 +39,7 @@ public class RevokedTypeAdapter implements JsonDeserializer<BlockchainBlock.Revo
         }
         
         String[] identityParts = identityStr.split(":");
-        if (identityParts.length != 4) {
+        if (identityParts.length != 2) {
             throw new JsonParseException(String.format("Bad format for BlockchainBlock.Revoked. Should have 4 parts, but found %s.", identityParts.length));
         }
 
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
new file mode 100644
index 00000000..e67c1d2b
--- /dev/null
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java
@@ -0,0 +1,51 @@
+package org.duniter.core.client.model.bma.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.google.gson.JsonParseException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.duniter.core.client.model.bma.BlockchainBlock;
+import org.duniter.core.client.model.bma.EndpointProtocol;
+import org.duniter.core.client.model.bma.NetworkPeering;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Created by blavenie on 07/12/16.
+ */
+public class EndpointDeserializer extends JsonDeserializer<NetworkPeering.Endpoint> {
+    @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
+                }
+            }
+        }
+
+        if (endpoint.protocol == null) {
+            endpoint.protocol = EndpointProtocol.UNDEFINED;
+        }
+
+        return endpoint;
+    }
+}
\ No newline at end of file
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/IdentityDeserializer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/IdentityDeserializer.java
new file mode 100644
index 00000000..49dd54b7
--- /dev/null
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/IdentityDeserializer.java
@@ -0,0 +1,40 @@
+package org.duniter.core.client.model.bma.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.google.gson.JsonParseException;
+import org.apache.commons.lang3.StringUtils;
+import org.duniter.core.client.model.bma.BlockchainBlock;
+
+import java.io.IOException;
+
+/**
+ * Created by blavenie on 07/12/16.
+ */
+public class IdentityDeserializer extends JsonDeserializer<BlockchainBlock.Identity> {
+    @Override
+    public BlockchainBlock.Identity deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+
+        String identityStr = jp.getText();
+        if (StringUtils.isBlank(identityStr)) {
+            return null;
+        }
+
+        String[] identityParts = identityStr.split(":");
+        if (identityParts.length != 4) {
+            throw new JsonParseException(String.format("Bad format for BlockchainBlock.Identity. Should have 4 parts, but found %s.", identityParts.length));
+        }
+
+        BlockchainBlock.Identity result = new BlockchainBlock.Identity();
+        int i = 0;
+
+        result.setPublicKey(identityParts[i++]);
+        result.setSignature(identityParts[i++]);
+        result.setBlockUid(identityParts[i++]);
+        result.setUserId(identityParts[i++]);
+
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JacksonUtils.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JacksonUtils.java
new file mode 100644
index 00000000..dfc44541
--- /dev/null
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JacksonUtils.java
@@ -0,0 +1,27 @@
+package org.duniter.core.client.model.bma.jackson;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.duniter.core.client.model.bma.BlockchainBlock;
+import org.duniter.core.client.model.bma.NetworkPeering;
+
+/**
+ * Created by blavenie on 07/12/16.
+ */
+public abstract class JacksonUtils extends SimpleModule {
+
+    public static ObjectMapper newObjectMapper() {
+        ObjectMapper objectMapper = new ObjectMapper();
+
+        // Configure deserializer
+        SimpleModule module = new SimpleModule();
+        module.addDeserializer(BlockchainBlock.Identity.class, new IdentityDeserializer());
+        module.addDeserializer(BlockchainBlock.Joiner.class, new JoinerDeserializer());
+        module.addDeserializer(BlockchainBlock.Revoked.class, new RevokedDeserializer());
+        module.addDeserializer(NetworkPeering.Endpoint.class, new EndpointDeserializer());
+
+        objectMapper.registerModule(module);
+
+        return objectMapper;
+    }
+}
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JoinerDeserializer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JoinerDeserializer.java
new file mode 100644
index 00000000..2cfa2a1e
--- /dev/null
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JoinerDeserializer.java
@@ -0,0 +1,40 @@
+package org.duniter.core.client.model.bma.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.google.gson.JsonParseException;
+import org.apache.commons.lang3.StringUtils;
+import org.duniter.core.client.model.bma.BlockchainBlock;
+
+import java.io.IOException;
+
+/**
+ * Created by blavenie on 07/12/16.
+ */
+public class JoinerDeserializer extends JsonDeserializer<BlockchainBlock.Joiner> {
+    @Override
+    public BlockchainBlock.Joiner deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+        String identityStr = jp.getText();
+        if (StringUtils.isBlank(identityStr)) {
+            return null;
+        }
+
+        String[] identityParts = identityStr.split(":");
+        if (identityParts.length != 5) {
+            throw new JsonParseException(String.format("Bad format for BlockchainBlock.Identity. Should have 5 parts, but found %s.", identityParts.length));
+        }
+
+        BlockchainBlock.Joiner result = new BlockchainBlock.Joiner();
+        int i = 0;
+
+        result.setPublicKey(identityParts[i++]);
+        result.setSignature(identityParts[i++]);
+        result.setMembershipBlockUid(identityParts[i++]);
+        result.setIdtyBlockUid(identityParts[i++]);
+        result.setUserId(identityParts[i++]);
+
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/RevokedDeserializer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/RevokedDeserializer.java
new file mode 100644
index 00000000..ab7661f5
--- /dev/null
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/RevokedDeserializer.java
@@ -0,0 +1,36 @@
+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 com.google.gson.JsonParseException;
+import org.apache.commons.lang3.StringUtils;
+import org.duniter.core.client.model.bma.BlockchainBlock;
+
+import java.io.IOException;
+
+/**
+ * Created by blavenie on 07/12/16.
+ */
+public class RevokedDeserializer extends JsonDeserializer<BlockchainBlock.Revoked> {
+    @Override
+    public BlockchainBlock.Revoked deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+        String identityStr = jp.getText();
+        if (StringUtils.isBlank(identityStr)) {
+            return null;
+        }
+
+        String[] identityParts = identityStr.split(":");
+        if (identityParts.length != 2) {
+            throw new JsonParseException(String.format("Bad format for BlockchainBlock.Revoked. Should have 4 parts, but found %s.", identityParts.length));
+        }
+
+        BlockchainBlock.Revoked result = new BlockchainBlock.Revoked();
+        int i = 0;
+
+        result.setSignature(identityParts[i++]);
+        result.setUserId(identityParts[i++]);
+
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Record.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Record.java
index 67d8cf86..65ad743c 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Record.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Record.java
@@ -35,7 +35,7 @@ public class Record {
     private String issuer;
     private String hash;
     private String signature;
-    private Integer time;
+    private Long time;
 
     public Record() {
     }
@@ -72,11 +72,11 @@ public class Record {
     }
 
 
-    public Integer getTime() {
+    public Long getTime() {
         return time;
     }
 
-    public void setTime(Integer time) {
+    public void setTime(Long time) {
         this.time = time;
     }
 
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 07480993..54aedaa3 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
@@ -22,16 +22,8 @@ package org.duniter.core.client.service;
  * #L%
  */
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Joiner;
-import com.google.gson.Gson;
-import org.apache.http.client.utils.URIBuilder;
-import org.duniter.core.beans.InitializingBean;
-import org.duniter.core.client.config.Configuration;
-import org.duniter.core.client.model.bma.Error;
-import org.duniter.core.client.model.bma.gson.GsonUtils;
-import org.duniter.core.client.model.local.Peer;
-import org.duniter.core.client.service.exception.*;
-import org.duniter.core.exception.TechnicalException;
 import org.apache.http.HttpResponse;
 import org.apache.http.HttpStatus;
 import org.apache.http.client.HttpClient;
@@ -39,8 +31,19 @@ import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.utils.URIBuilder;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
+import org.duniter.core.beans.InitializingBean;
+import org.duniter.core.client.config.Configuration;
+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.HttpBadRequestException;
+import org.duniter.core.client.service.exception.HttpConnectException;
+import org.duniter.core.client.service.exception.HttpNotFoundException;
+import org.duniter.core.client.service.exception.PeerConnectionException;
+import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.util.StringUtils;
 import org.nuiton.i18n.I18n;
 import org.slf4j.Logger;
@@ -62,7 +65,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
     public static final String URL_PEER_ALIVE = "/blockchain/parameters";
 
     protected Integer baseTimeOut;
-    protected Gson gson;
+    protected ObjectMapper objectMapper;
     protected HttpClient httpClient;
     protected Peer defaultPeer;
     private boolean debug;
@@ -75,7 +78,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
     @Override
     public void afterPropertiesSet() throws Exception {
         Configuration config = Configuration.instance();
-        this.gson = GsonUtils.newBuilder().create();
+        this.objectMapper = JacksonUtils.newObjectMapper();
         this.baseTimeOut = config.getNetworkTimeout();
         this.httpClient = createHttpClient();
     }
@@ -294,22 +297,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean
         // deserialize using gson
         else {
             try {
-                Reader reader = new InputStreamReader(content, StandardCharsets.UTF_8);
-                if (ResultClass != null) {
-                    result = gson.fromJson(reader, ResultClass);
-                }
-                else {
-                    result = null;
-                }
-            }
-            catch (com.google.gson.JsonSyntaxException e) {
-                if (content != null) {
-                    log.warn("Error while parsing JSON response: " + getContentAsString(content), e);
-                }
-                else {
-                    log.warn("Error while parsing JSON response", e);
-                }
-                throw new JsonSyntaxException(I18n.t("duniter4j.client.core.invalidResponse"), e);
+                result = objectMapper.readValue(content, ResultClass);
             }
             catch (Exception e) {
                 throw new TechnicalException(I18n.t("duniter4j.client.core.invalidResponse"), e);
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 8516a025..d3ccd755 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
@@ -91,7 +91,15 @@ public interface BlockchainRemoteService extends Service {
      * @param number the block number
      * @return
      */
-    BlockchainBlock getBlock(Peer peer, int number) throws BlockNotFoundException;
+    BlockchainBlock getBlock(Peer peer, long number) throws BlockNotFoundException;
+
+    /**
+     * Get block with TX
+     *
+     * @param peer   the peer to use for request
+     * @return
+     */
+    long[] getBlocksWithTx(Peer peer);
 
     /**
      * Retrieve a block, by id (from 0 to current) as JSON string
@@ -100,7 +108,7 @@ public interface BlockchainRemoteService extends Service {
      * @param number the block number
      * @return
      */
-    String getBlockAsJson(Peer peer, int number) throws BlockNotFoundException;
+    String getBlockAsJson(Peer peer, long number) throws BlockNotFoundException;
 
     /**
      * Retrieve a block, by id (from 0 to current) as JSON string
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 f277e5c7..966c112e 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
@@ -23,10 +23,7 @@ package org.duniter.core.client.service.bma;
  */
 
 import org.duniter.core.client.config.Configuration;
-import org.duniter.core.client.model.bma.BlockchainBlock;
-import org.duniter.core.client.model.bma.BlockchainMemberships;
-import org.duniter.core.client.model.bma.BlockchainParameters;
-import org.duniter.core.client.model.bma.Protocol;
+import org.duniter.core.client.model.bma.*;
 import org.duniter.core.client.model.bma.gson.JsonArrayParser;
 import org.duniter.core.client.model.local.Identity;
 import org.duniter.core.client.model.local.Peer;
@@ -66,10 +63,13 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
 
     public static final String URL_BLOCK = URL_BASE + "/block/%s";
 
+
     public static final String URL_BLOCKS_FROM = URL_BASE + "/blocks/%s/%s";
 
     public static final String URL_BLOCK_CURRENT = URL_BASE + "/current";
 
+    public static final String URL_BLOCK_WITH_TX = URL_BASE + "/with/tx";
+
     public static final String URL_BLOCK_WITH_UD = URL_BASE + "/with/ud";
 
     public static final String URL_MEMBERSHIP = URL_BASE + "/membership";
@@ -164,7 +164,7 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
 
 
     @Override
-    public BlockchainBlock getBlock(Peer peer, int number) throws BlockNotFoundException {
+    public BlockchainBlock getBlock(Peer peer, long number) throws BlockNotFoundException {
         // Get block from number
         String path = String.format(URL_BLOCK, number);
         try {
@@ -176,7 +176,19 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement
     }
 
     @Override
-    public String getBlockAsJson(Peer peer, int number) {
+    public long[] getBlocksWithTx(Peer peer) {
+        try {
+            Blocks blocks = executeRequest(peer, URL_BLOCK_WITH_TX, Blocks.class);
+            return (blocks == null || blocks.getResult() == null) ? new long[0] : blocks.getResult().getBlocks();
+        }
+        catch(HttpNotFoundException e) {
+            throw new TechnicalException(String.format("Error while getting blocks with TX on peer [%s]", peer));
+        }
+    }
+
+
+    @Override
+    public String getBlockAsJson(Peer peer, long number) {
         // get blockchain parameter
         String path = String.format(URL_BLOCK, number);
         try {
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 0548f391..5125270f 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
@@ -23,7 +23,9 @@ package org.duniter.core.client.service.bma;
  */
 
 
+import com.google.common.base.Joiner;
 import org.duniter.core.client.model.TxOutput;
+import org.duniter.core.client.model.bma.BlockchainBlock;
 import org.duniter.core.client.model.bma.Protocol;
 import org.duniter.core.client.model.bma.TxHistory;
 import org.duniter.core.client.model.bma.TxSource;
@@ -76,13 +78,16 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen
 
 	public String transfer(Wallet wallet, String destPubKey, long amount,
 						   String comment) throws InsufficientCreditException {
-		
+
+		// Get current block
+		BlockchainBlock currentBlock = executeRequest(wallet.getCurrencyId(), BlockchainRemoteServiceImpl.URL_BLOCK_CURRENT, BlockchainBlock.class);
+
 		// http post /tx/process
 		HttpPost httpPost = new HttpPost(
 				getPath(wallet.getCurrencyId(), URL_TX_PROCESS));
 
 		// compute transaction
-		String transaction = getSignedTransaction(wallet, destPubKey, 0, amount,
+		String transaction = getSignedTransaction(wallet, currentBlock, destPubKey, 0, amount,
                 comment);
 
 		if (log.isDebugEnabled()) {
@@ -212,6 +217,7 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen
 	/* -- internal methods -- */
 
 	public String getSignedTransaction(Wallet wallet,
+									   BlockchainBlock block,
 									   String destPubKey,
 									   int locktime,
 									   long amount,
@@ -229,16 +235,18 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen
 		TxSource.Source[] sources = sourceResults.getSources();
 		if (CollectionUtils.isEmpty(sources)) {
 			throw new InsufficientCreditException(
-					"Insufficient credit : no credit found.");
+					"Insufficient credit: no credit found.");
 		}
 
-		List<TxSource.Source> txInputs = new ArrayList<TxSource.Source>();
-		List<TxOutput> txOutputs = new ArrayList<TxOutput>();
-		computeTransactionInputsAndOuputs(wallet.getPubKeyHash(), destPubKey,
+		List<TxSource.Source> txInputs = new ArrayList<>();
+		List<TxOutput> txOutputs = new ArrayList<>();
+		computeTransactionInputsAndOuputs(block.getUnitbase(),
+				wallet.getPubKeyHash(), destPubKey,
 				sources, amount, txInputs, txOutputs);
 
 		String transaction = getTransaction(wallet.getCurrency(),
-				wallet.getPubKeyHash(), destPubKey, locktime, txInputs, txOutputs,
+				block.getNumber(), block.getHash(),
+				wallet.getPubKeyHash(), locktime, txInputs, txOutputs,
 				comment);
 
 		String signature = cryptoService.sign(transaction, wallet.getSecKey());
@@ -248,16 +256,18 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen
 	}
 
 	public String getTransaction(String currency,
+								 long blockNumber,
+								 String blockHash,
 								 String srcPubKey,
-								 String destPubKey,
 								 int locktime,
 								 List<TxSource.Source> inputs, List<TxOutput> outputs,
 			String comments) {
 
 		StringBuilder sb = new StringBuilder();
-		sb.append("Version: ").append(Protocol.VERSION).append("\n")
+		sb.append("Version: ").append(Protocol.TX_VERSION).append("\n")
 				.append("Type: ").append(Protocol.TYPE_TRANSACTION).append("\n")
 				.append("Currency: ").append(currency).append('\n')
+				.append("Blockstamp: ").append(blockNumber).append('-').append(blockHash).append("\n")
 				.append("Locktime: ").append(locktime).append('\n')
 				.append("Issuers:\n")
 				// add issuer pubkey
@@ -265,12 +275,18 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen
 
 		// Inputs coins
 		sb.append("Inputs:\n");
+		Joiner joiner = Joiner.on(':');
 		for (TxSource.Source input : inputs) {
-			// if D : D:PUBLIC_KEY:BLOCK_ID
-			// if T : T:T_HASH:T_INDEX
-			sb.append(input.getType()).append(':')
-					.append(input.getIdentifier()).append(':')
-					.append(input.getNoffset()).append('\n');
+			// if D : AMOUNT:BASE:D:PUBLIC_KEY:BLOCK_ID
+			// if T : AMOUNT:BASE:T:T_HASH:T_INDEX
+			joiner.appendTo(sb, new String[]{
+					String.valueOf(input.getAmount()),
+					String.valueOf(input.getBase()),
+					input.getType(),
+					input.getIdentifier(),
+					input.getNoffset()
+			});
+			sb.append('\n');
 		}
 
 		// Unlocks
@@ -296,15 +312,15 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen
 		return sb.toString();
 	}
 
-	public String getCompactTransaction(String currency, String srcPubKey,
-			String destPubKey, List<TxSource.Source> inputs, List<TxOutput> outputs,
+	public String getCompactTransaction(String srcPubKey,
+			List<TxSource.Source> inputs, List<TxOutput> outputs,
 			String comments) {
 
 		boolean hasComment = comments != null && comments.length() > 0;
 		StringBuilder sb = new StringBuilder();
 		sb.append("TX:")
 				// VERSION
-				.append(Protocol.VERSION).append(':')
+				.append(Protocol.TX_VERSION).append(':')
 				// NB_ISSUERS
 				.append("1:")
 				// NB_INPUTS
@@ -339,42 +355,144 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen
 		return sb.toString();
 	}
 
-	public void computeTransactionInputsAndOuputs(String srcPubKey,
+	public void computeTransactionInputsAndOuputs(int currentUnitBase,
+												  String srcPubKey,
 			String destPubKey, TxSource.Source[] sources, long amount,
-			List<TxSource.Source> inputs, List<TxOutput> outputs) throws InsufficientCreditException{
+			List<TxSource.Source> resultInputs, List<TxOutput> resultOutputs) throws InsufficientCreditException{
+
+		TxInputs inputs = new TxInputs();
+		inputs.amount = 0;
+		inputs.minBase = currentUnitBase;
+		inputs.maxBase = currentUnitBase + 1;
+
+		// Get inputs, starting to use current base sources
+		int amountBase = 0;
+		while (inputs.amount < amount && amountBase <= currentUnitBase) {
+			inputs = getInputs(sources, amount, currentUnitBase, currentUnitBase);
+
+			if (inputs.amount < amount) {
+				// try to reduce amount (replace last digits to zero)
+				amountBase++;
+				if (amountBase <= currentUnitBase) {
+					amount = truncBase(amount, amountBase);
+				}
+			}
+		}
+
+		if (inputs.amount < amount) {
+			throw new InsufficientCreditException("Insufficient credit");
+		}
+
+		// Avoid to get outputs on lower base
+		if (amountBase < inputs.minBase && !isBase(amount, inputs.minBase)) {
+			amount = truncBase(amount, inputs.minBase);
+			log.debug("TX Amount has been truncate to " + amount);
+		}
+		else if (amountBase > 0) {
+			log.debug("TX Amount has been truncate to " + amount);
+		}
+		resultInputs.addAll(inputs.sources);
 
 		long rest = amount;
-		long restForHimSelf = 0;
-
-		for (TxSource.Source source : sources) {
-			long srcAmount = source.getAmount();
-			inputs.add(source);
-			if (srcAmount >= rest) {
-				restForHimSelf = srcAmount - rest;
-				rest = 0;
-				break;
+		int outputBase = inputs.maxBase;
+		long outputAmount;
+		while(rest > 0) {
+			outputAmount = truncBase(rest, outputBase);
+			rest -= outputAmount;
+			if (outputAmount > 0) {
+				outputAmount = inversePowBase(outputAmount, outputBase);
+				TxOutput output = new TxOutput();
+				output.setAmount(outputAmount);
+				output.setBase(outputBase);
+				output.setPubKey(destPubKey);
+				resultOutputs.add(output);
 			}
-			rest -= srcAmount;
+			outputBase--;
 		}
-
-		if (rest > 0) {
-			throw new InsufficientCreditException(String.format(
-					"Insufficient credit. Need %s more units.", rest));
+		rest = inputs.amount - amount;
+		outputBase = inputs.maxBase;
+		while(rest > 0) {
+			outputAmount = truncBase(rest, outputBase);
+			rest -= outputAmount;
+			if (outputAmount > 0) {
+				outputAmount = inversePowBase(outputAmount, outputBase);
+				TxOutput output = new TxOutput();
+				output.setAmount(outputAmount);
+				output.setBase(outputBase);
+				output.setPubKey(srcPubKey);
+				resultOutputs.add(output);
+			}
+			outputBase--;
 		}
+	}
+
+	private long truncBase(long amount, int base) {
+		long pow = (long)Math.pow(10, base);
+		if (amount < pow) return pow;
+		return (long)(Math.floor(amount / pow ) * pow);
+	}
 
-		// outputs
-		{
-			TxOutput output = new TxOutput();
-			output.setPubKey(destPubKey);
-			output.setAmount(amount);
-			outputs.add(output);
+	private long powBase(long amount, int base) {
+		if (base <= 0) return amount;
+		return (long) (amount * Math.pow(10, base));
+	}
+
+	private long inversePowBase(long amount, int base) {
+		if (base <= 0) return amount;
+		return (long) (amount / Math.pow(10, base));
+	}
+
+	private boolean isBase(long amount, int base) {
+		if (base <= 0) return true;
+		if (amount < Math.pow(10, base)) return false;
+		String rest = "00000000" + amount;
+		long lastDigits = Integer.parseInt(rest.substring(rest.length()-base));
+		return lastDigits == 0; // no rest
+	}
+
+	private TxInputs getInputs(TxSource.Source[] availableSources, long amount, int outputBase, int filterBase) {
+		if (filterBase < 0) {
+			filterBase = outputBase;
 		}
-		if (restForHimSelf > 0) {
-			TxOutput output = new TxOutput();
-			output.setPubKey(srcPubKey);
-			output.setAmount(restForHimSelf);
-			outputs.add(output);
+		long sourcesAmount = 0;
+		TxInputs result = new TxInputs();
+		result.minBase = filterBase;
+		result.maxBase = filterBase;
+		for (TxSource.Source source: availableSources) {
+			if (source.getBase() == filterBase){
+				sourcesAmount += powBase(source.getAmount(), source.getBase());
+				result.sources.add(source);
+				// Stop if enough sources
+				if (sourcesAmount >= amount) {
+					break;
+				}
+			}
 		}
+
+		// IF not enough sources, get add inputs from lower base (recursively)
+		if (sourcesAmount < amount && filterBase > 0) {
+			filterBase -= 1;
+			long missingAmount = amount - sourcesAmount;
+			TxInputs lowerInputs = getInputs(availableSources, missingAmount, outputBase, filterBase);
+
+			// Add lower base inputs to result
+			if (lowerInputs.amount > 0) {
+				result.minBase = lowerInputs.minBase;
+				sourcesAmount += lowerInputs.amount;
+				result.sources.addAll(lowerInputs.sources);
+			}
+		}
+
+		result.amount = sourcesAmount;
+
+		return result;
+	}
+
+	private class TxInputs {
+		long amount;
+		int minBase;
+		int maxBase;
+		List<TxSource.Source> sources = new ArrayList<>();
 	}
 
 }
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 2f762ca7..97829efe 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
@@ -422,8 +422,8 @@ public class WotRemoteServiceImpl extends BaseRemoteServiceImpl implements WotRe
 
         target.setUid(source.getUid());
         target.setSelf(source.getSelf());
-        String timestamp = source.getMeta() != null ? source.getMeta().timestamp : null;
-        //FIXME target.setTimestamp(timestamp);
+        String timestamp = source.getMeta() != null ? source.getMeta().getTimestamp() : null;
+        target.setTimestamp(timestamp);
     }
 
     public String getSignedIdentity(String currency, byte[] pubKey, byte[] secKey, String uid, String blockUid) {
@@ -552,8 +552,8 @@ public class WotRemoteServiceImpl extends BaseRemoteServiceImpl implements WotRe
         // Read certified-by
         if (CollectionUtils.isNotEmpty(lookupResults.getResults())) {
             for (WotLookup.Result lookupResult: lookupResults.getResults()) {
-                if (lookupResult.signed != null) {
-                    for(WotLookup.SignedSignature lookupSignature : lookupResult.signed) {
+                if (lookupResult.getSigned() != null) {
+                    for(WotLookup.SignedSignature lookupSignature : lookupResult.getSigned()) {
                         Certification certifiedBy = toCertifiedByCerticication(lookupSignature);
 
                         // Set the currency Id
@@ -778,25 +778,25 @@ public class WotRemoteServiceImpl extends BaseRemoteServiceImpl implements WotRe
 
         Certification target = new Certification();
         // uid
-        target.setUid(source.uid);
+        target.setUid(source.getUid());
 
         // certifieb by
         target.setCertifiedBy(true);
 
-        if (source.meta != null) {
+        if (source.getMeta() != null) {
 
             // timestamp
-            String timestamp = source.meta != null ? source.meta.timestamp : null;
+            String timestamp = source.getMeta() != null ? source.getMeta().getTimestamp() : null;
             if (timestamp != null) {
                 //FIXME target.setTimestamp(timestamp.longValue());
             }
         }
 
         // Pubkey
-        target.setPubkey(source.pubkey);
+        target.setPubkey(source.getPubkey());
 
         // Is member
-        target.setMember(source.isMember);
+        target.setMember(source.isMember());
 
         // add to result list
         return target;
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 f9ce3c7e..a34d8bea 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
@@ -96,8 +96,7 @@ public class CurrencyRegistryRemoteServiceImpl extends BaseRemoteServiceImpl imp
         // get currency
         String jsonResponse;
         try {
-            String path = getPath(peer, URL_STATUS);
-            jsonResponse = executeRequest(peer, path, String.class);
+            jsonResponse = executeRequest(peer, URL_STATUS, String.class);
             int statusCode = GsonUtils.getValueFromJSONAsInt(jsonResponse, "status");
             return statusCode == HttpStatus.SC_OK;
         }
diff --git a/duniter4j-core-client/src/main/resources/i18n/duniter4j-core-client_fr_FR.properties b/duniter4j-core-client/src/main/resources/i18n/duniter4j-core-client_fr_FR.properties
index 94c99f6e..8e18b6c8 100644
--- a/duniter4j-core-client/src/main/resources/i18n/duniter4j-core-client_fr_FR.properties
+++ b/duniter4j-core-client/src/main/resources/i18n/duniter4j-core-client_fr_FR.properties
@@ -1,8 +1,8 @@
 duniter4j.client.authentication=Echec de la requete (Accès interdit ou non autorisé).
 duniter4j.client.core.connect=Echec de la connection au noeud Duniter [%s]
-duniter4j.client.core.emptyResponse=
-duniter4j.client.core.invalidResponse=
-duniter4j.client.core.timeout=
+duniter4j.client.core.emptyResponse=Réponse HTTP vide
+duniter4j.client.core.invalidResponse=Réponse HTTP non valide
+duniter4j.client.core.timeout=Délai d'attente de la requête dépassé
 duniter4j.client.notFound=Ressource non trouvée [%s]
 duniter4j.client.status=Echec de requete HTTP [%s] \: %s
 duniter4j.config=
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
new file mode 100644
index 00000000..0f96bc8b
--- /dev/null
+++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java
@@ -0,0 +1,73 @@
+package org.duniter.core.client.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 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);
+
+	@ClassRule
+	public static final TestResource resource = TestResource.create();
+	
+	private HttpService service;
+	private Peer peer;
+	
+	@Before
+	public void setUp() {
+		service = ServiceLocator.instance().getHttpService();
+		peer = createTestPeer();
+	}
+
+	@Test
+	public void connect() throws Exception {
+
+		service.connect(peer);
+	}
+
+
+	/* -- internal methods */
+
+    protected Peer createTestPeer() {
+        Peer peer = new Peer(
+                Configuration.instance().getNodeHost(),
+                Configuration.instance().getNodePort());
+
+        return peer;
+    }
+}
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 6cc37273..67ac086e 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
@@ -86,6 +86,25 @@ public class BlockchainRemoteServiceTest {
         }
     }
 
+    @Test
+    public void getBlockWithTx() throws Exception {
+
+        long[] blocks = service.getBlocksWithTx(createTestPeer());
+        if (blocks == null) return;
+
+        // Check first block with TX
+        BlockchainBlock result = service.getBlock(createTestPeer(), blocks[0]);
+        Assert.assertNotNull(result);
+        Assert.assertNotNull(result.getTransactions());
+        Assert.assertTrue(result.getTransactions().length > 0);
+
+        // Check last block with TX
+        result = service.getBlock(createTestPeer(), blocks[blocks.length-1]);
+        Assert.assertNotNull(result);
+        Assert.assertNotNull(result.getTransactions());
+        Assert.assertTrue(result.getTransactions().length > 0);
+    }
+
     @Test
     public void getBlocksAsJson() throws Exception {
 
@@ -115,13 +134,10 @@ public class BlockchainRemoteServiceTest {
 
         isWebSocketNewBlockReceived = false;
 
-        service.addNewBlockListener(createTestPeer(), new WebsocketClientEndpoint.MessageHandler() {
-            @Override
-            public void handleMessage(String message) {
-                BlockchainBlock block = GsonUtils.newBuilder().create().fromJson(message, BlockchainBlock.class);
-                log.debug("Received block #" + block.getNumber());
-                isWebSocketNewBlockReceived = true;
-            }
+        service.addNewBlockListener(createTestPeer(), (message) -> {
+            BlockchainBlock block = GsonUtils.newBuilder().create().fromJson(message, BlockchainBlock.class);
+            log.debug("Received block #" + block.getNumber());
+            isWebSocketNewBlockReceived = true;
         });
 
         int count = 0;
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 8f0f5fcb..8d8950c6 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
@@ -49,8 +49,6 @@ public class TransactionRemoteServiceTest {
 	}
 
 	@Test
-	@Ignore
-	//FIXME : get Wrong unit base for outputs - see implementation on Cesium
 	public void transfer() throws Exception {
 
 		service.transfer(
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 db373205..5cbbd36e 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
@@ -97,7 +97,7 @@ public class WotRemoteServiceTest {
 		for (WotCertification.Certification cert : result.getCertifications()) {
 			Assert.assertNotNull(cert.getUid());
 
-			WotCertification.CertTime certTime = cert.getCert_time();
+			WotCertification.CertTime certTime = cert.getCertTime();
 			Assert.assertNotNull(certTime);
 			Assert.assertTrue(certTime.getBlock() >= 0);
 			Assert.assertNotNull(certTime.getMedianTime() >= 0);
@@ -124,7 +124,7 @@ public class WotRemoteServiceTest {
 		for (WotCertification.Certification cert : result.getCertifications()) {
 			Assert.assertNotNull(cert.getUid());
 
-			WotCertification.CertTime certTime = cert.getCert_time();
+			WotCertification.CertTime certTime = cert.getCertTime();
 			Assert.assertNotNull(certTime);
 			Assert.assertTrue(certTime.getBlock() >= 0);
 			Assert.assertNotNull(certTime.getMedianTime() >= 0);
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 4eae98cf..a420819a 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,11 +1,11 @@
 #duniter4j.node.host=metab.ucoin.io
 #duniter4j.node.host=metab.ucoin.fr
 
-#duniter4j.node.host=192.168.0.28
-#duniter4j.node.port=9202
+duniter4j.node.host=test-net.duniter.fr
+duniter4j.node.port=9201
 
-duniter4j.node.host=cgeek.fr
-duniter4j.node.port=9330
+#duniter4j.node.host=cgeek.fr
+#duniter4j.node.port=9330
 
 
 duniter4j.node.elasticsearch.host=localhost
diff --git a/duniter4j-core-client/src/test/resources/log4j.properties b/duniter4j-core-client/src/test/resources/log4j.properties
index ba8baa96..a3e37387 100644
--- a/duniter4j-core-client/src/test/resources/log4j.properties
+++ b/duniter4j-core-client/src/test/resources/log4j.properties
@@ -9,7 +9,7 @@ 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=DEBUG
 log4j.logger.org.duniter.core.client.service.bma=DEBUG
 log4j.logger.org.duniter.core.beans=WARN
 
diff --git a/duniter4j-core-shared/LICENSE b/duniter4j-core-shared/LICENSE
index 94a9ed02..6df63bc4 100644
--- a/duniter4j-core-shared/LICENSE
+++ b/duniter4j-core-shared/LICENSE
@@ -552,7 +552,7 @@ License would be to refrain entirely from conveying the Program.
   13. Use with the GNU Affero General Public License.
 
   Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
+permission to reference or combine any covered work with a work licensed
 under version 3 of the GNU Affero General Public License into a single
 combined work, and to convey the resulting work.  The terms of this
 License will continue to apply to the part which is the covered work,
diff --git a/duniter4j-es-core/LICENSE b/duniter4j-es-core/LICENSE
index 94a9ed02..6df63bc4 100644
--- a/duniter4j-es-core/LICENSE
+++ b/duniter4j-es-core/LICENSE
@@ -552,7 +552,7 @@ License would be to refrain entirely from conveying the Program.
   13. Use with the GNU Affero General Public License.
 
   Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
+permission to reference or combine any covered work with a work licensed
 under version 3 of the GNU Affero General Public License into a single
 combined work, and to convey the resulting work.  The terms of this
 License will continue to apply to the part which is the covered work,
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 fe53f1c7..1931d541 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
@@ -285,6 +285,10 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> {
         return settings.getAsBoolean("duniter.ws.enable", Boolean.TRUE);
     }
 
+    public String[] getWebSocketChangesListenSource()  {
+        return settings.getAsArray("duniter.ws.changes.listenSource", new String[]{"*"});
+    }
+
     /* protected methods */
 
     protected void initI18n() throws IOException {
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/CurrencyService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/CurrencyService.java
index 0cd8989c..1ab515e3 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/CurrencyService.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/CurrencyService.java
@@ -167,7 +167,7 @@ public class CurrencyService extends AbstractService {
      */
     public Currency indexCurrencyFromPeer(Peer peer) {
         BlockchainParameters parameters = blockchainRemoteService.getParameters(peer);
-        BlockchainBlock firstBlock = blockchainRemoteService.getBlock(peer, 0);
+        BlockchainBlock firstBlock = blockchainRemoteService.getBlock(peer, 0l);
         BlockchainBlock currentBlock = blockchainRemoteService.getCurrentBlock(peer);
         long lastUD = blockchainRemoteService.getLastUD(peer);
 
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeEvent.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeEvent.java
index 99d6fd7a..2b26e303 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeEvent.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeEvent.java
@@ -38,9 +38,20 @@ package org.duniter.elasticsearch.service.changes;
     limitations under the License.
 */
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.JsonSyntaxException;
+import org.duniter.core.exception.TechnicalException;
+import org.duniter.elasticsearch.exception.InvalidFormatException;
 import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.json.JsonXContent;
 import org.joda.time.DateTime;
 
+import java.io.IOException;
+
 public class ChangeEvent {
     private final String id;
     private final String index;
@@ -93,4 +104,51 @@ public class ChangeEvent {
     }
 
 
+    public String toJson() {
+        try {
+            XContentBuilder builder = new XContentBuilder(JsonXContent.jsonXContent, new BytesStreamOutput());
+            builder.startObject()
+                    .field("_index", getIndex())
+                    .field("_type", getType())
+                    .field("_id", getId())
+                    .field("_timestamp", getTimestamp())
+                    .field("_version", getVersion())
+                    .field("_operation", getOperation().toString());
+            if (getSource() != null) {
+                builder.rawField("_source", getSource());
+            }
+            builder.endObject();
+
+            return builder.string();
+        } catch (IOException e) {
+            throw new TechnicalException("Error while generating JSON from change event", e);
+        }
+    }
+
+    public static ChangeEvent fromJson(String json) {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            JsonNode actualObj = objectMapper.readTree(json);
+            String index = actualObj.get("_index").asText();
+            String type = actualObj.get("_type").asText();
+            String id = actualObj.get("_id").asText();
+            DateTime timestamp = new DateTime(actualObj.get("_timestamp").asLong());
+            ChangeEvent.Operation operation = ChangeEvent.Operation.valueOf(actualObj.get("_operation").asText());
+            long version = actualObj.get("_version").asLong();
+
+            JsonNode sourceNode = actualObj.get("_source");
+            BytesReference source = null;
+            if (sourceNode != null) {
+                // TODO : fill bytes reference from source
+                //source = sourceNode.
+            }
+
+            ChangeEvent event = new ChangeEvent(index, type, id, timestamp, operation, version, source);
+            return event;
+        } catch (IOException | JsonSyntaxException e) {
+            throw new InvalidFormatException("Invalid record JSON: " + e.getMessage(), e);
+        }
+    }
+
+
 }
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeListener.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeListener.java
deleted file mode 100644
index af8ba091..00000000
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeListener.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.duniter.elasticsearch.service.changes;
-
-public interface ChangeListener {
-    String getId();
-    void onChanges(String message);
-}
\ No newline at end of file
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeService.java
index 56487c38..7b5d3bc4 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeService.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeService.java
@@ -38,13 +38,11 @@ package org.duniter.elasticsearch.service.changes;
     limitations under the License.
 */
 
+import org.duniter.core.util.CollectionUtils;
 import org.elasticsearch.common.inject.Inject;
-import org.elasticsearch.common.io.stream.BytesStreamOutput;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.Loggers;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.json.JsonXContent;
 import org.elasticsearch.index.engine.Engine;
 import org.elasticsearch.index.indexing.IndexingOperationListener;
 import org.elasticsearch.index.shard.IndexShard;
@@ -52,14 +50,16 @@ import org.elasticsearch.indices.IndicesLifecycle;
 import org.elasticsearch.indices.IndicesService;
 import org.joda.time.DateTime;
 
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 public class ChangeService {
 
+    public interface ChangeListener {
+        String getId();
+        void onChange(ChangeEvent change);
+        Collection<ChangeSource> getChangeSources();
+    }
+
     private static final String SETTING_PRIMARY_SHARD_ONLY = "duniter.changes.primaryShardOnly";
     private static final String SETTING_LISTEN_SOURCE = "duniter.changes.listenSource";
 
@@ -67,14 +67,13 @@ public class ChangeService {
 
     private static final Map<String, ChangeListener> LISTENERS = new HashMap<>();
 
+    private static Map<String, ChangeSource> LISTENERS_SOURCES = new HashMap<>();
+    private static Map<String, Integer> LISTENERS_SOURCES_USAGE_COUNT = new HashMap<>();
+
     @Inject
     public ChangeService(final Settings settings, IndicesService indicesService) {
         final boolean allShards = !settings.getAsBoolean(SETTING_PRIMARY_SHARD_ONLY, Boolean.FALSE);
-        final String[] sourcesStr = settings.getAsArray(SETTING_LISTEN_SOURCE, new String[]{"*"});
-        final Set<ChangeSource> sources = new HashSet<>();
-        for(String sourceStr : sourcesStr) {
-            sources.add(new ChangeSource(sourceStr));
-        }
+
 
         indicesService.indicesLifecycle().addListener(new IndicesLifecycle.Listener() {
             @Override
@@ -85,6 +84,10 @@ public class ChangeService {
                     indexShard.indexingService().addListener(new IndexingOperationListener() {
                         @Override
                         public void postCreate(Engine.Create create) {
+                            if (!hasListener(indexName, create.type(), create.id())) {
+                                return;
+                            }
+
                             ChangeEvent change=new ChangeEvent(
                                     indexName,
                                     create.type(),
@@ -100,6 +103,10 @@ public class ChangeService {
 
                         @Override
                         public void postDelete(Engine.Delete delete) {
+                            if (!hasListener(indexName, delete.type(), delete.id())) {
+                                return;
+                            }
+
                             ChangeEvent change=new ChangeEvent(
                                     indexName,
                                     delete.type(),
@@ -115,6 +122,9 @@ public class ChangeService {
 
                         @Override
                         public void postIndex(Engine.Index index) {
+                            if (!hasListener(indexName, index.type(), index.id())) {
+                                return;
+                            }
 
                             ChangeEvent change=new ChangeEvent(
                                     indexName,
@@ -129,25 +139,24 @@ public class ChangeService {
                             addChange(change);
                         }
 
-                        private boolean filter(String index, String type, String id, ChangeSource source) {
-                            if (source.getIndices() != null && !source.getIndices().contains(index)) {
-                                return false;
-                            }
-
-                            if (source.getTypes() != null && !source.getTypes().contains(type)) {
-                                return false;
-                            }
+                        private boolean hasListener(String index, String type, String id) {
+                            if (LISTENERS_SOURCES.isEmpty()) return false;
 
-                            if (source.getIds() != null && !source.getIds().contains(id)) {
-                                return false;
+                            for (ChangeSource source : LISTENERS_SOURCES.values()) {
+                                if (source.apply(index, type, id)) {
+                                    return true;
+                                }
                             }
 
-                            return true;
+                            return false;
                         }
 
-                        private boolean filter(String index, ChangeEvent change) {
+                        private boolean apply(ChangeListener listener, ChangeEvent change) {
+                            Collection<ChangeSource> sources = listener.getChangeSources();
+                            if (CollectionUtils.isEmpty(sources)) return true;
+
                             for (ChangeSource source : sources) {
-                                if (filter(index, change.getType(), change.getId(), source)) {
+                                if (source.apply(change.getIndex(), change.getType(), change.getId())) {
                                     return true;
                                 }
                             }
@@ -156,40 +165,14 @@ public class ChangeService {
                         }
 
                         private void addChange(ChangeEvent change) {
-
-                            if (!filter(indexName, change)) {
-                                return;
-                            }
-
-
-                            String message;
-                            try {
-                                XContentBuilder builder = new XContentBuilder(JsonXContent.jsonXContent, new BytesStreamOutput());
-                                builder.startObject()
-                                        .field("_index", indexName)
-                                        .field("_type", change.getType())
-                                        .field("_id", change.getId())
-                                        .field("_timestamp", change.getTimestamp())
-                                        .field("_version", change.getVersion())
-                                        .field("_operation", change.getOperation().toString());
-                                if (change.getSource() != null) {
-                                    builder.rawField("_source", change.getSource());
-                                }
-                                builder.endObject();
-
-                                message = builder.string();
-                            } catch (IOException e) {
-                                log.error("Failed to write JSON", e);
-                                return;
-                            }
-
                             for (ChangeListener listener : LISTENERS.values()) {
-                                try {
-                                    listener.onChanges(message);
-                                } catch (Exception e) {
-                                    log.error("Failed to send message", e);
+                                if (apply(listener, change)) {
+                                    try {
+                                        listener.onChange(change);
+                                    } catch (Exception e) {
+                                        log.error("Failed to send message", e);
+                                    }
                                 }
-
                             }
 
                         }
@@ -202,10 +185,41 @@ public class ChangeService {
 
     public static void registerListener(ChangeListener listener) {
         LISTENERS.put(listener.getId(), listener);
+
+        // Update sources
+        if (CollectionUtils.isNotEmpty(listener.getChangeSources())) {
+            for (ChangeSource source: listener.getChangeSources()) {
+                String sourceKey = source.toString();
+                if (!LISTENERS_SOURCES.containsKey(sourceKey)) {
+                    LISTENERS_SOURCES.put(sourceKey, source);
+                    LISTENERS_SOURCES_USAGE_COUNT.put(sourceKey, 1);
+                }
+                else {
+                    LISTENERS_SOURCES_USAGE_COUNT.put(sourceKey, LISTENERS_SOURCES_USAGE_COUNT.get(sourceKey)+1);
+                }
+            }
+        }
     }
 
     public static void unregisterListener(ChangeListener listener) {
         LISTENERS.remove(listener.getId());
+
+        // Update sources
+        if (CollectionUtils.isNotEmpty(listener.getChangeSources())) {
+            for (ChangeSource source: listener.getChangeSources()) {
+                String sourceKey = source.toString();
+                if (LISTENERS_SOURCES.containsKey(sourceKey)) {
+                    int usageCount = LISTENERS_SOURCES_USAGE_COUNT.get(sourceKey) - 1;
+                    if (usageCount > 0) {
+                        LISTENERS_SOURCES_USAGE_COUNT.put(sourceKey, usageCount);
+                    }
+                    else {
+                        LISTENERS_SOURCES.remove(sourceKey);
+                        LISTENERS_SOURCES_USAGE_COUNT.remove(sourceKey);
+                    }
+                }
+            }
+        }
     }
 
 
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeSource.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeSource.java
index 6f79a507..982a3454 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeSource.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeSource.java
@@ -38,7 +38,10 @@ package org.duniter.elasticsearch.service.changes;
     limitations under the License.
 */
 
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
+import org.duniter.core.util.StringUtils;
 
 import java.util.Set;
 
@@ -65,6 +68,17 @@ public class ChangeSource {
         }
     }
 
+    public ChangeSource(String index, String type) {
+        this(index, type, null);
+    }
+
+    public ChangeSource(String index, String type, String id) {
+        Preconditions.checkArgument(StringUtils.isNotBlank(index));
+        indices = index.equals("*") ? null : ImmutableSet.of(index);
+        types = StringUtils.isBlank(type) || type.equals("*") ? null : ImmutableSet.of(type);
+        ids = StringUtils.isBlank(id) || id.equals("*") ? null : ImmutableSet.of(id);
+    }
+
     public Set<String> getIds() {
         return ids;
     }
@@ -77,4 +91,51 @@ public class ChangeSource {
         return types;
     }
 
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+
+        // Add indices
+        Joiner joiner = Joiner.on(',');
+        if (indices == null) {
+            sb.append('*');
+        }
+        else {
+            joiner.appendTo(sb, indices);
+        }
+
+        // Add types
+        if (types == null) {
+            if (ids != null) {
+                sb.append("/*");
+            }
+        }
+        else {
+            sb.append('/');
+            joiner.appendTo(sb, types);
+        }
+
+        // Add ids
+        if (ids != null) {
+            sb.append('/');
+            joiner.appendTo(sb, ids);
+        }
+        return sb.toString();
+    }
+
+    public boolean apply(String index, String type, String id) {
+        if (indices != null && !indices.contains(index)) {
+            return false;
+        }
+
+        if (types != null && !types.contains(type)) {
+            return false;
+        }
+
+        if (ids != null && !ids.contains(id)) {
+            return false;
+        }
+
+        return true;
+    }
+
 }
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeUtils.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeUtils.java
deleted file mode 100644
index cfe6a16c..00000000
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/changes/ChangeUtils.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.duniter.elasticsearch.service.changes;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.gson.JsonSyntaxException;
-import org.duniter.core.exception.TechnicalException;
-import org.duniter.elasticsearch.exception.InvalidFormatException;
-import org.elasticsearch.common.bytes.BytesReference;
-import org.elasticsearch.common.io.stream.BytesStreamOutput;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.json.JsonXContent;
-import org.joda.time.DateTime;
-
-import java.io.IOException;
-
-/**
- * Created by blavenie on 30/11/16.
- */
-public class ChangeUtils {
-
-    public static ChangeEvent fromJson(ObjectMapper objectMapper, String json) {
-        try {
-            JsonNode actualObj = objectMapper.readTree(json);
-            String index = actualObj.get("_index").asText();
-            String type = actualObj.get("_type").asText();
-            String id = actualObj.get("_id").asText();
-            DateTime timestamp = new DateTime(actualObj.get("_timestamp").asLong());
-            ChangeEvent.Operation operation = ChangeEvent.Operation.valueOf(actualObj.get("_operation").asText());
-            long version = actualObj.get("_version").asLong();
-
-            JsonNode sourceNode = actualObj.get("_source");
-            BytesReference source = null;
-            if (sourceNode != null) {
-                // TODO : fill bytes reference from source
-                //source = sourceNode.
-            }
-
-            ChangeEvent event = new ChangeEvent(index, type, id, timestamp, operation, version, source);
-            return event;
-        } catch (IOException | JsonSyntaxException e) {
-            throw new InvalidFormatException("Invalid record JSON: " + e.getMessage(), e);
-        }
-    }
-
-    public static String toJson(ChangeEvent change) {
-        try {
-            XContentBuilder builder = new XContentBuilder(JsonXContent.jsonXContent, new BytesStreamOutput());
-            builder.startObject()
-                    .field("_index", change.getIndex())
-                    .field("_type", change.getType())
-                    .field("_id", change.getId())
-                    .field("_timestamp", change.getTimestamp())
-                    .field("_version", change.getVersion())
-                    .field("_operation", change.getOperation().toString());
-            if (change.getSource() != null) {
-                builder.rawField("_source", change.getSource());
-            }
-            builder.endObject();
-
-            return builder.string();
-        } catch (IOException e) {
-            throw new TechnicalException("Error while generating JSON from change event", e);
-        }
-    }
-}
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/changes/WebSocketChangeEndPoint.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketChangesEndPoint.java
similarity index 70%
rename from duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/changes/WebSocketChangeEndPoint.java
rename to duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketChangesEndPoint.java
index b99e117e..8d7b02f6 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/changes/WebSocketChangeEndPoint.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketChangesEndPoint.java
@@ -1,4 +1,4 @@
-package org.duniter.elasticsearch.websocket.changes;
+package org.duniter.elasticsearch.websocket;
 
 /*
  * #%L
@@ -38,24 +38,36 @@ package org.duniter.elasticsearch.websocket.changes;
     limitations under the License.
 */
 
-import org.duniter.elasticsearch.service.changes.ChangeListener;
+import org.duniter.elasticsearch.PluginSettings;
+import org.duniter.elasticsearch.service.changes.ChangeEvent;
 import org.duniter.elasticsearch.service.changes.ChangeService;
-import org.duniter.elasticsearch.websocket.WebSocketServer;
+import org.duniter.elasticsearch.service.changes.ChangeSource;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.Loggers;
 
 import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 
 @ServerEndpoint(value = "/_changes")
-public class WebSocketChangeEndPoint implements ChangeListener{
+public class WebSocketChangesEndPoint implements ChangeService.ChangeListener{
+
+    public static Collection<ChangeSource> SOURCES = null;
 
     public static class Init {
 
         @Inject
-        public Init(WebSocketServer webSocketServer) {
-            webSocketServer.addEndPoint(WebSocketChangeEndPoint.class);
+        public Init(WebSocketServer webSocketServer, PluginSettings pluginSettings) {
+            webSocketServer.addEndPoint(WebSocketChangesEndPoint.class);
+            final String[] sourcesStr = pluginSettings.getWebSocketChangesListenSource();
+            List<ChangeSource> sources = new ArrayList<>();
+            for(String sourceStr : sourcesStr) {
+                sources.add(new ChangeSource(sourceStr));
+            }
+            SOURCES = sources;
         }
     }
 
@@ -70,8 +82,8 @@ public class WebSocketChangeEndPoint implements ChangeListener{
     }
 
     @Override
-    public void onChanges(String message) {
-        session.getAsyncRemote().sendText(message);
+    public void onChange(ChangeEvent changeEvent) {
+        session.getAsyncRemote().sendText(changeEvent.toJson());
     }
 
     @Override
@@ -79,6 +91,11 @@ public class WebSocketChangeEndPoint implements ChangeListener{
         return session == null ? null : session.getId();
     }
 
+    @Override
+    public Collection<ChangeSource> getChangeSources() {
+        return SOURCES;
+    }
+
     @OnMessage
     public void onMessage(String message) {
         log.debug("Received message: "+message);
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketModule.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketModule.java
index 717933a1..7152c435 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketModule.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketModule.java
@@ -38,13 +38,12 @@ package org.duniter.elasticsearch.websocket;
     limitations under the License.
 */
 
-import org.duniter.elasticsearch.websocket.changes.WebSocketChangeEndPoint;
 import org.elasticsearch.common.inject.AbstractModule;
 
 public class WebSocketModule extends AbstractModule {
     @Override
     protected void configure() {
         bind(WebSocketServer.class).asEagerSingleton();
-        bind(WebSocketChangeEndPoint.Init.class).asEagerSingleton();
+        bind(WebSocketChangesEndPoint.Init.class).asEagerSingleton();
     }
 }
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketServer.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketServer.java
index 598fc81f..c0ac3524 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketServer.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/websocket/WebSocketServer.java
@@ -41,7 +41,6 @@ package org.duniter.elasticsearch.websocket;
 import org.duniter.core.exception.TechnicalException;
 import org.duniter.elasticsearch.PluginSettings;
 import org.duniter.elasticsearch.threadpool.ThreadPool;
-import org.duniter.elasticsearch.websocket.changes.WebSocketChangeEndPoint;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.Loggers;
diff --git a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/MarketService.java b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/MarketService.java
index 1236558c..2e2a4a3b 100644
--- a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/MarketService.java
+++ b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/MarketService.java
@@ -438,7 +438,7 @@ public class MarketService extends AbstractService {
                             issuerTitle != null ? issuerTitle : issuer.substring(0, 8),
                             recordTitle)
                     .setRecipient(recordIssuer)
-                    .setLink(INDEX, RECORD_TYPE, recordId)
+                    .setReference(INDEX, RECORD_TYPE, recordId)
                     .build());
         }
     }
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEvent.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEvent.java
index 5e8e4921..601c819d 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEvent.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEvent.java
@@ -23,7 +23,9 @@ package org.duniter.elasticsearch.user.model;
  */
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Preconditions;
 import org.duniter.core.client.model.elasticsearch.Record;
 import org.duniter.core.exception.TechnicalException;
 import org.nuiton.i18n.I18n;
@@ -57,7 +59,7 @@ public class UserEvent extends Record {
     public static final String PROPERTY_CODE="code";
     public static final String PROPERTY_MESSAGE="message";
     public static final String PROPERTY_PARAMS="params";
-    public static final String PROPERTY_LINK="link";
+    public static final String PROPERTY_LINK="reference";
     public static final String PROPERTY_RECIPIENT="recipient";
 
 
@@ -71,7 +73,7 @@ public class UserEvent extends Record {
 
     private String[] params;
 
-    private UserEventLink link;
+    private Reference reference;
 
     public UserEvent() {
         super();
@@ -91,7 +93,7 @@ public class UserEvent extends Record {
         this.type = another.getType();
         this.code = another.getCode();
         this.params = another.getParams();
-        this.link = (another.getLink() != null) ? new UserEventLink(another.getLink()) : null;
+        this.reference = (another.getReference() != null) ? new Reference(another.getReference()) : null;
         this.message = another.getMessage();
         this.recipient = another.getRecipient();
     }
@@ -112,8 +114,8 @@ public class UserEvent extends Record {
         return params;
     }
 
-    public UserEventLink getLink() {
-        return link;
+    public Reference getReference() {
+        return reference;
     }
 
     public String getLocalizedMessage(Locale locale) {
@@ -136,8 +138,8 @@ public class UserEvent extends Record {
         this.params = params;
     }
 
-    public void setLink(UserEventLink link) {
-        this.link = link;
+    public void setReference(Reference reference) {
+        this.reference = reference;
     }
 
     public String getRecipient() {
@@ -152,6 +154,7 @@ public class UserEvent extends Record {
     public String toJson() {
         try {
             ObjectMapper mapper = new ObjectMapper();
+            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
             return mapper.writeValueAsString(this);
         } catch(Exception e) {
             throw new TechnicalException(e);
@@ -165,6 +168,10 @@ public class UserEvent extends Record {
         return copy.toJson();
     }
 
+    private static long getDefaultTime() {
+        return Math.round(1d * System.currentTimeMillis() / 1000);
+    }
+
     public static class Builder {
 
         private UserEvent result;
@@ -193,13 +200,24 @@ public class UserEvent extends Record {
             return this;
         }
 
-        public Builder setLink(String index, String type, String id) {
-            result.setLink(new UserEventLink(index, type, id));
+        public Builder setReference(String index, String type, String id) {
+            result.setReference(new Reference(index, type, id));
+            return this;
+        }
+
+        public Builder setReferenceHash(String hash) {
+            Preconditions.checkNotNull(result.getReference(), "No reference set. Please call setReference() first");
+            result.getReference().setHash(hash);
             return this;
         }
 
-        public Builder setLink(String index, String type, String id, String anchor) {
-            result.setLink(new UserEventLink(index, type, id, anchor));
+        public Builder setReference(String index, String type, String id, String anchor) {
+            result.setReference(new Reference(index, type, id, anchor));
+            return this;
+        }
+
+        public Builder setTime(long time) {
+            result.setTime(time);
             return this;
         }
 
@@ -211,7 +229,68 @@ public class UserEvent extends Record {
         }
     }
 
-    private static int getDefaultTime() {
-        return Math.round(1f * System.currentTimeMillis() / 1000);
+
+
+    public static class Reference {
+
+        private String index;
+
+        private String type;
+
+        private String id;
+
+        private String anchor;
+
+        private String hash;
+
+        public Reference() {
+        }
+
+        public Reference(String index, String type, String id) {
+            this(index, type, id, null);
+        }
+
+        public Reference(String index, String type, String id, String anchor) {
+            this.index = index;
+            this.type = type;
+            this.id = id;
+            this.anchor = anchor;
+        }
+
+        public Reference(Reference another) {
+            this.index = another.getIndex();
+            this.type = another.getType();
+            this.id = another.getId();
+            this.hash = another.getHash();
+            this.anchor = another.getAnchor();
+        }
+
+        public String getIndex() {
+            return index;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public String getAnchor() {
+            return anchor;
+        }
+
+        public void setAnchor(String anchor) {
+            this.anchor = anchor;
+        }
+
+        public String getHash() {
+            return hash;
+        }
+
+        public void setHash(String hash) {
+            this.hash = hash;
+        }
     }
 }
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
index 39e4d4e0..fbb06fab 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
@@ -28,6 +28,15 @@ package org.duniter.elasticsearch.user.model;
 public enum UserEventCodes {
 
     NODE_STARTED,
-    CREATE_DOC
+    CREATE_DOC,
+
+    // Membership state
+    MEMBER_JOIN,
+    MEMBER_LEAVE,
+    MEMBER_ACTIVE,
+
+    // TX
+    TX_SENT,
+    TX_RECEIVED
 
 }
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventLink.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventLink.java
deleted file mode 100644
index fb03cd71..00000000
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventLink.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package org.duniter.elasticsearch.user.model;
-
-/*
- * #%L
- * Duniter4j :: ElasticSearch Plugin
- * %%
- * Copyright (C) 2014 - 2016 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%
- */
-
-/**
- * Created by blavenie on 29/11/16.
- */
-public class UserEventLink {
-
-    private String index;
-
-    private String type;
-
-    private String id;
-
-    private String anchor;
-
-    public UserEventLink() {
-    }
-
-    public UserEventLink(String index, String type, String id) {
-        this(index, type, id, null);
-    }
-
-    public UserEventLink(String index, String type, String id, String anchor) {
-        this.index = index;
-        this.type = type;
-        this.id = id;
-        this.anchor = anchor;
-    }
-
-    public UserEventLink(UserEventLink another) {
-        this.index = another.getIndex();
-        this.type = another.getType();
-        this.id = another.getId();
-        this.anchor = another.getAnchor();
-    }
-
-    public String getIndex() {
-        return index;
-    }
-
-    public String getType() {
-        return type;
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public String getAnchor() {
-        return anchor;
-    }
-}
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/BlockchainUserEventService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/BlockchainUserEventService.java
new file mode 100644
index 00000000..1b2976c2
--- /dev/null
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/BlockchainUserEventService.java
@@ -0,0 +1,190 @@
+package org.duniter.elasticsearch.user.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.databind.ObjectMapper;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.duniter.core.client.model.bma.BlockchainBlock;
+import org.duniter.core.client.model.bma.jackson.JacksonUtils;
+import org.duniter.core.exception.TechnicalException;
+import org.duniter.core.service.CryptoService;
+import org.duniter.core.util.CollectionUtils;
+import org.duniter.elasticsearch.PluginSettings;
+import org.duniter.elasticsearch.service.AbstractService;
+import org.duniter.elasticsearch.service.BlockchainService;
+import org.duniter.elasticsearch.service.changes.ChangeEvent;
+import org.duniter.elasticsearch.service.changes.ChangeService;
+import org.duniter.elasticsearch.service.changes.ChangeSource;
+import org.duniter.elasticsearch.user.model.UserEvent;
+import org.duniter.elasticsearch.user.model.UserEventCodes;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.inject.Inject;
+import org.nuiton.i18n.I18n;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Created by Benoit on 30/03/2015.
+ */
+public class BlockchainUserEventService extends AbstractService implements ChangeService.ChangeListener {
+
+    public final UserEventService userEventService;
+
+    public final ObjectMapper objectMapper;
+
+    public final List<ChangeSource> changeListenSources;
+
+    public final Joiner simpleJoiner = Joiner.on(',');
+
+    @Inject
+    public BlockchainUserEventService(Client client, PluginSettings settings, CryptoService cryptoService,
+                                      UserEventService userEventService) {
+        super("duniter.user.event.blockchain", client, settings, cryptoService);
+        this.userEventService = userEventService;
+        this.objectMapper = JacksonUtils.newObjectMapper();
+        this.changeListenSources = ImmutableList.of(new ChangeSource("*", BlockchainService.BLOCK_TYPE));
+        ChangeService.registerListener(this);
+    }
+
+    @Override
+    public String getId() {
+        return "duniter.user.event.blockchain";
+    }
+
+    @Override
+    public void onChange(ChangeEvent change) {
+        if (change.getSource() == null) return;
+
+        try {
+            BlockchainBlock block = objectMapper.readValue(change.getSource().streamInput(), BlockchainBlock.class);
+
+            switch (change.getOperation()) {
+                case INDEX:
+                    processBlockIndex(block);
+                    break;
+
+                // on DELETE : remove user event on block (using link
+                case DELETE:
+                    processBlockDelete(block);
+                    break;
+            }
+
+        }
+        catch(IOException e) {
+            throw new TechnicalException(String.format("Unable to parse received block %s", change.getId()), e);
+        }
+
+        //logger.info("receiveing block change: " + change.toJson());
+    }
+
+    @Override
+    public Collection<ChangeSource> getChangeSources() {
+        return changeListenSources;
+    }
+
+    /* -- internal method -- */
+
+    private void processBlockIndex(BlockchainBlock block) {
+        // Joiners
+        if (CollectionUtils.isNotEmpty(block.getJoiners())) {
+            for (BlockchainBlock.Joiner joiner: block.getJoiners()) {
+                notifyUserEvent(block, joiner.getPublicKey(), UserEventCodes.MEMBER_JOIN, I18n.n("duniter.user.event.ms.join"), block.getCurrency());
+            }
+        }
+
+        // Leavers
+        if (CollectionUtils.isNotEmpty(block.getLeavers())) {
+            for (BlockchainBlock.Joiner leaver: block.getJoiners()) {
+                notifyUserEvent(block, leaver.getPublicKey(), UserEventCodes.MEMBER_LEAVE, I18n.n("duniter.user.event.ms.leave"), block.getCurrency());
+            }
+        }
+
+        // Actives
+        if (CollectionUtils.isNotEmpty(block.getActives())) {
+            for (BlockchainBlock.Joiner active: block.getActives()) {
+                notifyUserEvent(block, active.getPublicKey(), UserEventCodes.MEMBER_ACTIVE, I18n.n("duniter.user.event.ms.active"), block.getCurrency());
+            }
+        }
+
+        // Tx
+        if (CollectionUtils.isNotEmpty(block.getTransactions())) {
+            for (BlockchainBlock.Transaction tx: block.getTransactions()) {
+                processTx(block, tx);
+            }
+        }
+    }
+
+    private void processTx(BlockchainBlock block, BlockchainBlock.Transaction tx) {
+        Set<String> senders = ImmutableSet.copyOf(tx.getIssuers());
+
+
+        // Received
+        // TODO get profile name
+        String sendersStr = simpleJoiner.join(senders);
+        Set<String> receivers = new HashSet<>();
+        for (String output : tx.getOutputs()) {
+            String[] parts = output.split(":");
+            if (parts.length >= 3 && parts[2].startsWith("SIG(")) {
+                String receiver = parts[2].substring(4, parts[2].length() - 1);
+                if (!senders.contains(receiver) && !receivers.contains(receiver)) {
+                    notifyUserEvent(block, receiver, UserEventCodes.TX_RECEIVED, I18n.n("duniter.user.event.tx.received"), sendersStr);
+                    receivers.add(receiver);
+                }
+            }
+        }
+
+
+        // Sent
+        // TODO get profile name
+        String receiverStr = simpleJoiner.join(receivers);
+        for (String sender:senders) {
+            notifyUserEvent(block, sender, UserEventCodes.TX_SENT, I18n.n("duniter.user.event.tx.sent"), receiverStr);
+        }
+
+        // TODO : index this TX in a special index ?
+    }
+
+    private void notifyUserEvent(BlockchainBlock block, String pubkey, UserEventCodes code, String message, String... params) {
+        UserEvent event = UserEvent.newBuilder(UserEvent.EventType.INFO, code.name())
+                .setRecipient(pubkey)
+                .setMessage(message, params)
+                .setTime(block.getMedianTime())
+                .setReference(block.getCurrency(), BlockchainService.BLOCK_TYPE, String.valueOf(block.getNumber()))
+                .setReferenceHash(block.getHash())
+                .build();
+        userEventService.notifyUser(event);
+    }
+
+    private void processBlockDelete(BlockchainBlock block) {
+
+
+        //userEventService.deleteUserEventByReference()
+    }
+}
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java
index 30e1b815..bcb7164e 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java
@@ -28,10 +28,13 @@ import org.elasticsearch.common.inject.Module;
 public class ServiceModule extends AbstractModule implements Module {
 
     @Override protected void configure() {
-        bind(MessageService.class).asEagerSingleton();
         bind(HistoryService.class).asEagerSingleton();
+
+        bind(MessageService.class).asEagerSingleton();
         bind(UserService.class).asEagerSingleton();
         bind(UserEventService.class).asEagerSingleton();
+        bind(BlockchainUserEventService.class).asEagerSingleton();
+
         bind(SynchroService.class).asEagerSingleton();
     }
 
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java
index 1f002846..c961281e 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java
@@ -87,7 +87,7 @@ public class UserEventService extends AbstractService {
     public UserEventService(Client client, PluginSettings settings, CryptoService cryptoService,
                             MailService mailService,
                             ThreadPool threadPool) {
-        super("duniter.event." + INDEX, client, settings, cryptoService);
+        super("duniter.user.event", client, settings, cryptoService);
         this.mailService = mailService;
         this.threadPool = threadPool;
         this.nodeKeyPair = getNodeKeyPairOrNull(pluginSettings);
@@ -102,44 +102,20 @@ public class UserEventService extends AbstractService {
      * Notify cluster admin
      */
     public void notifyAdmin(UserEvent event) {
-
-        UserProfile adminProfile;
-        if (StringUtils.isNotBlank(nodePubkey)) {
-            adminProfile = getUserProfile(nodePubkey, UserProfile.PROPERTY_EMAIL, UserProfile.PROPERTY_LOCALE);
-        }
-        else {
-            adminProfile = new UserProfile();
-        }
-
-        // Add new event to index
-        Locale locale = StringUtils.isNotBlank(adminProfile.getLocale()) ?
-                new Locale(adminProfile.getLocale()) :
-                I18n.getDefaultLocale();
-        if (StringUtils.isNotBlank(nodePubkey)) {
-            event.setRecipient(nodePubkey);
-            indexEvent(locale, event);
-        }
-
-        // Send email to admin
-        String adminEmail = StringUtils.isNotBlank(adminProfile.getEmail()) ?
-                adminProfile.getEmail() :
-                pluginSettings.getMailAdmin();
-        if (StringUtils.isNotBlank(adminEmail)) {
-            String subjectPrefix = pluginSettings.getMailSubjectPrefix();
-            sendEmail(adminEmail,
-                    I18n.l(locale, "duniter4j.event.subject."+event.getType().name(), subjectPrefix),
-                    event.getLocalizedMessage(locale));
-        }
+        // async
+        //threadPool.schedule(() -> {
+            doNotifyAdmin(event);
+        //});
     }
 
     /**
      * Notify a user
      */
     public void notifyUser(UserEvent event) {
-        // Notify user
+        // async
         threadPool.schedule(() -> {
             doNotifyUser(event);
-        }, TimeValue.timeValueMillis(100));
+        }, TimeValue.timeValueMillis(500));
     }
 
     public String indexEvent(Locale locale, UserEvent event) {
@@ -235,8 +211,8 @@ public class UserEventService extends AbstractService {
                     .field("index", "not_analyzed")
                     .endObject()
 
-                    // link
-                    .startObject("link")
+                    // reference
+                    .startObject("reference")
                         .field("type", "nested")
                         .field("dynamic", "false")
                         .startObject("properties")
@@ -252,6 +228,10 @@ public class UserEventService extends AbstractService {
                                 .field("type", "string")
                                 .field("index", "not_analyzed")
                             .endObject()
+                            .startObject("hash")
+                                .field("type", "string")
+                                .field("index", "not_analyzed")
+                            .endObject()
                         .endObject()
                     .endObject()
 
@@ -265,6 +245,18 @@ public class UserEventService extends AbstractService {
                     .field("type", "string")
                     .endObject()
 
+                    // hash
+                    .startObject("hash")
+                    .field("type", "string")
+                    .field("index", "not_analyzed")
+                    .endObject()
+
+                    // signature
+                    .startObject("signature")
+                    .field("type", "string")
+                    .field("index", "not_analyzed")
+                    .endObject()
+
                     .endObject()
                     .endObject().endObject();
 
@@ -312,6 +304,40 @@ public class UserEventService extends AbstractService {
         return CryptoUtils.encodeBase58(nodeKeyPair.getPubKey());
     }
 
+    /**
+     * Notify cluster admin
+     */
+    public void doNotifyAdmin(UserEvent event) {
+
+        UserProfile adminProfile;
+        if (StringUtils.isNotBlank(nodePubkey)) {
+            adminProfile = getUserProfile(nodePubkey, UserProfile.PROPERTY_EMAIL, UserProfile.PROPERTY_LOCALE);
+        }
+        else {
+            adminProfile = new UserProfile();
+        }
+
+        // Add new event to index
+        Locale locale = StringUtils.isNotBlank(adminProfile.getLocale()) ?
+                new Locale(adminProfile.getLocale()) :
+                I18n.getDefaultLocale();
+        if (StringUtils.isNotBlank(nodePubkey)) {
+            event.setRecipient(nodePubkey);
+            indexEvent(locale, event);
+        }
+
+        // Send email to admin
+        String adminEmail = StringUtils.isNotBlank(adminProfile.getEmail()) ?
+                adminProfile.getEmail() :
+                pluginSettings.getMailAdmin();
+        if (StringUtils.isNotBlank(adminEmail)) {
+            String subjectPrefix = pluginSettings.getMailSubjectPrefix();
+            sendEmail(adminEmail,
+                    I18n.l(locale, "duniter4j.event.subject."+event.getType().name(), subjectPrefix),
+                    event.getLocalizedMessage(locale));
+        }
+    }
+
     /**
      * Notify a user
      */
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
index 0cfbf566..5333f4e4 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
@@ -50,15 +50,13 @@ public class UserService extends AbstractService {
 
     public static final String INDEX = "user";
     public static final String PROFILE_TYPE = "profile";
-    public static final String EVENT_TYPE = "profile";
     public static final String SETTINGS_TYPE = "settings";
 
     @Inject
     public UserService(Client client,
                        PluginSettings settings,
-                       CryptoService cryptoService,
-                       MailService mailService) {
-        super("gchange." + INDEX, client, settings,cryptoService);
+                       CryptoService cryptoService) {
+        super("duniter." + INDEX, client, settings,cryptoService);
     }
 
     /**
diff --git a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties
index d884f6ba..f5143c94 100644
--- a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties
+++ b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties
@@ -1,5 +1,12 @@
-duniter.event.NODE_STARTED=
-duniter4j.event.NODE_STARTED=Node started on cluster Duniter4j ES [%s]
+duniter.event.NODE_STARTED=Node started on cluster Duniter4j ES [%s]
+duniter.user.event.active=
+duniter.user.event.join=
+duniter.user.event.leave=
+duniter.user.event.ms.active=
+duniter.user.event.ms.join=
+duniter.user.event.ms.leave=
+duniter.user.event.tx.received=
+duniter.user.event.tx.sent=
 duniter4j.event.subject.ERROR=[%s] Error message
 duniter4j.event.subject.INFO=[%s] Information message
 duniter4j.event.subject.WARN=[%s] Warning message
diff --git a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties
index e179adfd..57a43dce 100644
--- a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties
+++ b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties
@@ -1,5 +1,9 @@
-duniter.event.NODE_STARTED=
-duniter4j.event.NODE_STARTED=Noeud démarré sur le cluster Duniter4j ES [%s]
+duniter.event.NODE_STARTED=Noeud démarré sur le cluster Duniter4j ES [%s]
+duniter.user.event.ms.active=Votre adhésion comme membre a bien été renouvellée
+duniter.user.event.ms.join=Vous êtes maintenant membre de la monnaie
+duniter.user.event.ms.leave=Votre adhésion comme membre à expirée
+duniter.user.event.tx.received=Vous avez recu un paiement de %s
+duniter.user.event.tx.sent=Votre paiement à %s a bien été executé
 duniter4j.event.subject.ERROR=%s Message d'erreur
 duniter4j.event.subject.INFO=%s Message d'information
 duniter4j.event.subject.WARN=%s Message d'avertissement
-- 
GitLab