From 1fb29cabcdd2d99fb825cd939c1e5c314df59f79 Mon Sep 17 00:00:00 2001
From: librelois <c@elo.tf>
Date: Fri, 13 Nov 2020 22:17:29 +0100
Subject: [PATCH] [ref] rust-libs: modularization of rust server

---
 Cargo.lock                                    |  47 ++-
 Cargo.toml                                    |   4 +-
 app/lib/dal/fileDAL.ts                        |   8 +-
 neon/native/server.d.ts                       |   8 +-
 neon/native/src/crypto.rs                     |   8 +-
 neon/native/src/server.rs                     |  77 +++--
 rust-libs/duniter-conf/Cargo.toml             |  10 +
 rust-libs/duniter-conf/src/gva_conf.rs        |  96 ++++++
 rust-libs/duniter-conf/src/lib.rs             |  46 +++
 rust-libs/duniter-dbs/src/cm_v1.rs            |   2 +-
 rust-libs/duniter-dbs/src/lib.rs              |  14 +
 rust-libs/duniter-gva/src/lib.rs              | 253 ---------------
 rust-libs/duniter-mempools/src/lib.rs         |   7 +-
 rust-libs/duniter-module/Cargo.toml           |  20 ++
 rust-libs/duniter-module/src/lib.rs           | 165 ++++++++++
 rust-libs/duniter-server/Cargo.toml           |   7 +-
 rust-libs/duniter-server/src/lib.rs           |  95 +++---
 .../{ => modules}/duniter-gva/Cargo.toml      |  14 +-
 .../{ => modules}/duniter-gva/src/entities.rs |   0
 .../duniter-gva/src/entities/tx_gva.rs        |   0
 .../duniter-gva/src/entities/ud_gva.rs        |   0
 .../duniter-gva/src/inputs_validators.rs      |   0
 rust-libs/modules/duniter-gva/src/lib.rs      | 287 ++++++++++++++++++
 .../duniter-gva/src/mutations.rs              |   0
 .../{ => modules}/duniter-gva/src/queries.rs  |   2 +-
 .../duniter-gva/src/queries/gen_txs.rs        |   0
 .../duniter-gva/src/queries/txs_history.rs    |   0
 .../duniter-gva/src/queries/uds.rs            |   0
 .../duniter-gva/src/queries/utxos.rs          |   0
 .../{ => modules}/duniter-gva/src/schema.rs   |   1 -
 .../duniter-gva/src/subscriptions.rs          |   9 +-
 .../{ => modules}/duniter-gva/src/warp_.rs    |   4 +-
 server.ts                                     |  37 +--
 33 files changed, 825 insertions(+), 396 deletions(-)
 create mode 100644 rust-libs/duniter-conf/Cargo.toml
 create mode 100644 rust-libs/duniter-conf/src/gva_conf.rs
 create mode 100644 rust-libs/duniter-conf/src/lib.rs
 delete mode 100644 rust-libs/duniter-gva/src/lib.rs
 create mode 100644 rust-libs/duniter-module/Cargo.toml
 create mode 100644 rust-libs/duniter-module/src/lib.rs
 rename rust-libs/{ => modules}/duniter-gva/Cargo.toml (58%)
 rename rust-libs/{ => modules}/duniter-gva/src/entities.rs (100%)
 rename rust-libs/{ => modules}/duniter-gva/src/entities/tx_gva.rs (100%)
 rename rust-libs/{ => modules}/duniter-gva/src/entities/ud_gva.rs (100%)
 rename rust-libs/{ => modules}/duniter-gva/src/inputs_validators.rs (100%)
 create mode 100644 rust-libs/modules/duniter-gva/src/lib.rs
 rename rust-libs/{ => modules}/duniter-gva/src/mutations.rs (100%)
 rename rust-libs/{ => modules}/duniter-gva/src/queries.rs (96%)
 rename rust-libs/{ => modules}/duniter-gva/src/queries/gen_txs.rs (100%)
 rename rust-libs/{ => modules}/duniter-gva/src/queries/txs_history.rs (100%)
 rename rust-libs/{ => modules}/duniter-gva/src/queries/uds.rs (100%)
 rename rust-libs/{ => modules}/duniter-gva/src/queries/utxos.rs (100%)
 rename rust-libs/{ => modules}/duniter-gva/src/schema.rs (97%)
 rename rust-libs/{ => modules}/duniter-gva/src/subscriptions.rs (90%)
 rename rust-libs/{ => modules}/duniter-gva/src/warp_.rs (98%)

diff --git a/Cargo.lock b/Cargo.lock
index ac57f4423..8e70bf2a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -51,9 +51,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.33"
+version = "1.0.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c"
+checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7"
 
 [[package]]
 name = "arc-swap"
@@ -1051,6 +1051,14 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "duniter-conf"
+version = "0.1.0"
+dependencies = [
+ "dubp",
+ "serde",
+]
+
 [[package]]
 name = "duniter-dbex"
 version = "0.1.0"
@@ -1123,10 +1131,13 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-graphql",
+ "async-trait",
  "dubp",
+ "duniter-conf",
  "duniter-dbs",
  "duniter-dbs-read-ops",
  "duniter-mempools",
+ "duniter-module",
  "fast-threadpool",
  "flume",
  "futures",
@@ -1168,20 +1179,40 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "duniter-module"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "dubp",
+ "duniter-conf",
+ "duniter-dbs",
+ "duniter-mempools",
+ "fast-threadpool",
+ "paste",
+ "tokio",
+]
+
 [[package]]
 name = "duniter-server"
 version = "1.8.1"
 dependencies = [
+ "anyhow",
  "dubp",
+ "duniter-conf",
  "duniter-dbs",
  "duniter-dbs-read-ops",
  "duniter-dbs-write-ops",
  "duniter-gva",
  "duniter-mempools",
+ "duniter-module",
  "fast-threadpool",
  "flume",
  "log",
+ "paste",
  "resiter",
+ "tokio",
  "unwrap",
 ]
 
@@ -3463,6 +3494,18 @@ dependencies = [
  "num_cpus",
  "pin-project-lite",
  "slab",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 006d7cf59..57b22316f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,12 +32,14 @@ members = [
     "rust-bins/duniter-dbex",
     "rust-bins/xtask",
     "rust-libs/dubp-wot",
+    "rust-libs/duniter-conf",
     "rust-libs/duniter-dbs",
     "rust-libs/duniter-dbs-read-ops",
     "rust-libs/duniter-dbs-write-ops",
-    "rust-libs/duniter-gva",
     "rust-libs/duniter-mempools",
+    "rust-libs/duniter-module",
     "rust-libs/duniter-server",
+    "rust-libs/modules/duniter-gva",
     "rust-libs/tools/kv_typed"
 ]
 
diff --git a/app/lib/dal/fileDAL.ts b/app/lib/dal/fileDAL.ts
index 0353b7117..bf7f13b95 100644
--- a/app/lib/dal/fileDAL.ts
+++ b/app/lib/dal/fileDAL.ts
@@ -234,12 +234,12 @@ export class FileDAL implements ServerDAO {
   }
 
   initRustServer(conf: ConfDTO, commandName: string | null = null) {
-    let selfPubkey = conf.pair ? conf.pair.pub : null;
+    let selfKeypair = conf.pair ? conf.pair.sec : null;
     let rustServerConf = {
       command: commandName,
       currency: conf.currency || "",
       gva: conf.gva,
-      selfPubkey,
+      selfKeypair,
       txsMempoolSize:
         conf.txsMempoolSize || constants.SANDBOX_SIZE_TRANSACTIONS,
     };
@@ -250,6 +250,10 @@ export class FileDAL implements ServerDAO {
     }
   }
 
+  getRustEndpoints(): string[] {
+    return this.rustServer.getSelfEndpoints();
+  }
+
   getDBVersion() {
     return this.metaDAL.getVersion();
   }
diff --git a/neon/native/server.d.ts b/neon/native/server.d.ts
index 0e194c095..4f001ec48 100644
--- a/neon/native/server.d.ts
+++ b/neon/native/server.d.ts
@@ -6,7 +6,7 @@ export class RustServerConf {
     command: string | null
     currency: string
     gva: GvaConf | undefined
-    selfPubkey: string | null
+    selfKeypair: string | null
     txsMempoolSize: number
 }
 
@@ -15,6 +15,11 @@ export class GvaConf {
     port?: number
     path?: string;
     subscriptionsPath?: string;
+    remoteHost?: string
+    remotePort?: number
+    remotePath?: string;
+    remoteSubscriptionsPath?: string;
+    remoteTls?: boolean;
 }
 
 export class PeerCard {
@@ -90,6 +95,7 @@ export class RustServer {
     addPendingTx(tx: TransactionDTOV10): void;
     getMempoolTxsFreeRooms(): number;
     getNewPendingTxs(): TransactionDTOV10[];
+    getSelfEndpoints(): string[];
     getTransactionsHistory(pubkey: string): TxsHistory;
     getTransactionsPending(versionMin: number, medianTime: number): TransactionDTOV10[];
     getTxByHash(hash: string): TransactionDTOV10 | null;
diff --git a/neon/native/src/crypto.rs b/neon/native/src/crypto.rs
index 079916b1a..9430b118e 100644
--- a/neon/native/src/crypto.rs
+++ b/neon/native/src/crypto.rs
@@ -92,7 +92,7 @@ declare_types! {
                         .downcast::<JsString>()
                         .or_throw(&mut cx)?
                         .value();
-                        into_neon_res(&mut cx, keypair_from_expanded_base58_secret_key(&expanded_base58_secret_key))
+                        into_neon_res(&mut cx, keypair_from_expanded_base58_secret_key(&expanded_base58_secret_key).map(|kp| kp.generate_signator()))
                 } else if arg0.is_a::<JsBuffer>() {
                     let seed_js_buffer = arg0
                         .downcast::<JsBuffer>()
@@ -135,9 +135,9 @@ declare_types! {
     }
 }
 
-fn keypair_from_expanded_base58_secret_key(
+pub(crate) fn keypair_from_expanded_base58_secret_key(
     expanded_base58_secret_key: &str,
-) -> Result<Ed25519Signator, &'static str> {
+) -> Result<Ed25519KeyPair, &'static str> {
     let bytes = bs58::decode(expanded_base58_secret_key)
         .into_vec()
         .map_err(|_| "fail to decode b58")?;
@@ -152,7 +152,7 @@ fn keypair_from_expanded_base58_secret_key(
     //let expected_pubkey = Ed25519PublicKey::try_from(pubkey_bytes.as_ref());
 
     if keypair.public_key().as_ref()[..32] == pubkey_bytes {
-        Ok(keypair.generate_signator())
+        Ok(keypair)
     } else {
         Err("corrupted keypair")
     }
diff --git a/neon/native/src/server.rs b/neon/native/src/server.rs
index 5bf99530d..769318dd0 100644
--- a/neon/native/src/server.rs
+++ b/neon/native/src/server.rs
@@ -14,14 +14,14 @@
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 use crate::into_neon_res;
-use dubp::common::crypto::hashs::Hash;
 use dubp::common::crypto::keys::{ed25519::PublicKey, PublicKey as _};
 use dubp::documents::{
     prelude::*,
     transaction::{TransactionDocumentV10, TransactionDocumentV10Stringified},
 };
 use dubp::documents_parser::prelude::*;
-use duniter_server::{DuniterServer, DuniterServerConf, GvaConf, PeerCardStringified};
+use dubp::{common::crypto::hashs::Hash, crypto::keys::ed25519::Ed25519KeyPair};
+use duniter_server::{DuniterConf, DuniterServer, GvaConf, PeerCardStringified};
 use neon::declare_types;
 use neon::prelude::*;
 use serde::{Deserialize, Serialize};
@@ -39,35 +39,18 @@ declare_types! {
 
             let rust_server_conf_stringified: RustServerConfStringified = neon_serde::from_value(&mut cx, rust_server_conf_js)?;
 
-            let gva_conf = if let Some(gva_conf_stringified) = rust_server_conf_stringified.gva {
-                let mut gva_conf = GvaConf::default();
-                if let Some(host) = gva_conf_stringified.host {
-                    gva_conf.host(host);
-                }
-                if let Some(port) = gva_conf_stringified.port {
-                    gva_conf.port(port);
-                }
-                if let Some(path) = gva_conf_stringified.path {
-                    gva_conf.path(path);
-                }
-                if let Some(subscriptions_path) = gva_conf_stringified.subscriptions_path {
-                    gva_conf.subscriptions_path(subscriptions_path);
-                }
-                Some(gva_conf)
-            } else {
-                None
-            };
+            let gva_conf = rust_server_conf_stringified.gva;
             let command_name = rust_server_conf_stringified.command_name;
             let currency = rust_server_conf_stringified.currency;
-            let server_pubkey = if let Some(self_pubkey_str) = rust_server_conf_stringified.self_pubkey {
-                into_neon_res(&mut cx, PublicKey::from_base58(&self_pubkey_str))?
+            let server_pubkey = if let Some(self_keypair_str) = rust_server_conf_stringified.self_keypair {
+                into_neon_res(&mut cx, crate::crypto::keypair_from_expanded_base58_secret_key(&self_keypair_str))?
             } else {
-                PublicKey::default()
+                Ed25519KeyPair::generate_random().expect("fail to gen random keyypair")
             };
             let txs_mempool_size = rust_server_conf_stringified.txs_mempool_size as usize;
-            let conf = DuniterServerConf {
+            let conf = DuniterConf {
                 gva: gva_conf,
-                self_pubkey: server_pubkey,
+                self_key_pair: server_pubkey,
                 txs_mempool_size
             };
 
@@ -90,13 +73,14 @@ declare_types! {
             } else {
                 None
             };
-            if let Some(home_path) = home_path_opt {
-                let server = DuniterServer::start(command_name, conf, currency, Some(home_path.as_path()), std::env!("CARGO_PKG_VERSION"));
-                Ok(RustServer { server })
-            } else {
-                let server = DuniterServer::start(command_name, conf, currency, None, std::env!("CARGO_PKG_VERSION"));
-                Ok(RustServer { server })
-            }
+            into_neon_res(
+                &mut cx,
+                if let Some(home_path) = home_path_opt {
+                    DuniterServer::start(command_name, conf, currency, Some(home_path.as_path()), std::env!("CARGO_PKG_VERSION"))
+                } else {
+                    DuniterServer::start(command_name, conf, currency, None, std::env!("CARGO_PKG_VERSION"))
+                }.map(|server| RustServer { server })
+            )
         }
         method acceptNewTx(mut cx) {
             let tx_js = cx.argument::<JsValue>(0)?;
@@ -114,6 +98,22 @@ declare_types! {
             }.map(|accepted| cx.boolean(accepted).upcast());
             into_neon_res(&mut cx, res)
         }
+        method getSelfEndpoints(mut cx) {
+            let this = cx.this();
+            let res = {
+                let guard = cx.lock();
+                let server = this.borrow(&guard);
+                server.server.get_self_endpoints()
+            }.map(|endpoints| {
+                let js_array = JsArray::new(&mut cx, endpoints.len() as u32);
+                for (i, ep) in endpoints.iter().enumerate() {
+                    let js_string = cx.string(ep);
+                    js_array.set(&mut cx, i as u32, js_string).expect("fail to convert Vec<String> to JsArray");
+                }
+                js_array.upcast()
+            });
+            into_neon_res(&mut cx, res)
+        }
         method getTxByHash(mut cx) {
             let hash_str = cx.argument::<JsString>(0)?.value();
             let hash = into_neon_res(&mut cx, Hash::from_hex(&hash_str))?;
@@ -320,20 +320,11 @@ declare_types! {
 struct RustServerConfStringified {
     command_name: Option<String>,
     currency: String,
-    gva: Option<GvaConfStringified>,
-    self_pubkey: Option<String>,
+    gva: Option<GvaConf>,
+    self_keypair: Option<String>,
     txs_mempool_size: u32,
 }
 
-#[derive(Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-struct GvaConfStringified {
-    host: Option<String>,
-    port: Option<u16>,
-    path: Option<String>,
-    subscriptions_path: Option<String>,
-}
-
 #[derive(Deserialize, Serialize)]
 struct TxsHistoryStringified {
     sent: Vec<DbTx>,
diff --git a/rust-libs/duniter-conf/Cargo.toml b/rust-libs/duniter-conf/Cargo.toml
new file mode 100644
index 000000000..71826bc24
--- /dev/null
+++ b/rust-libs/duniter-conf/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "duniter-conf"
+version = "0.1.0"
+authors = ["librelois <elois@duniter.org>"]
+license = "AGPL-3.0"
+edition = "2018"
+
+[dependencies]
+dubp = { version = "0.30.0" }
+serde = { version = "1.0.105", features = ["derive"] }
diff --git a/rust-libs/duniter-conf/src/gva_conf.rs b/rust-libs/duniter-conf/src/gva_conf.rs
new file mode 100644
index 000000000..4d83d3119
--- /dev/null
+++ b/rust-libs/duniter-conf/src/gva_conf.rs
@@ -0,0 +1,96 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use crate::*;
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GvaConf {
+    host: Option<String>,
+    port: Option<u16>,
+    path: Option<String>,
+    subscriptions_path: Option<String>,
+    remote_host: Option<String>,
+    remote_port: Option<u16>,
+    remote_path: Option<String>,
+    remote_subscriptions_path: Option<String>,
+    remote_tls: Option<bool>,
+}
+
+impl GvaConf {
+    pub fn get_host(&self) -> String {
+        self.host
+            .to_owned()
+            .unwrap_or_else(|| "localhost".to_owned())
+    }
+    pub fn get_port(&self) -> u16 {
+        self.port.unwrap_or(30901)
+    }
+    pub fn get_path(&self) -> String {
+        if let Some(mut path) = self.path.clone() {
+            if path.starts_with('/') {
+                path.remove(0);
+                path
+            } else {
+                path
+            }
+        } else {
+            "localhost".to_owned()
+        }
+    }
+    pub fn get_subscriptions_path(&self) -> String {
+        if let Some(mut subscriptions_path) = self.subscriptions_path.clone() {
+            if subscriptions_path.starts_with('/') {
+                subscriptions_path.remove(0);
+                subscriptions_path
+            } else {
+                subscriptions_path
+            }
+        } else {
+            "localhost".to_owned()
+        }
+    }
+    pub fn get_remote_host(&self) -> String {
+        if let Some(ref remote_host) = self.remote_host {
+            remote_host.to_owned()
+        } else {
+            self.get_host()
+        }
+    }
+    pub fn get_remote_port(&self) -> u16 {
+        if let Some(remote_port) = self.remote_port {
+            remote_port
+        } else {
+            self.get_port()
+        }
+    }
+    pub fn get_remote_path(&self) -> String {
+        if let Some(ref remote_path) = self.remote_path {
+            remote_path.to_owned()
+        } else {
+            self.get_path()
+        }
+    }
+    pub fn get_remote_subscriptions_path(&self) -> String {
+        if let Some(ref remote_subscriptions_path) = self.remote_subscriptions_path {
+            remote_subscriptions_path.to_owned()
+        } else {
+            self.get_subscriptions_path()
+        }
+    }
+    pub fn get_remote_tls(&self) -> bool {
+        self.remote_tls.unwrap_or(false)
+    }
+}
diff --git a/rust-libs/duniter-conf/src/lib.rs b/rust-libs/duniter-conf/src/lib.rs
new file mode 100644
index 000000000..a966b2ab8
--- /dev/null
+++ b/rust-libs/duniter-conf/src/lib.rs
@@ -0,0 +1,46 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+#![deny(
+    clippy::unwrap_used,
+    missing_copy_implementations,
+    trivial_casts,
+    trivial_numeric_casts,
+    unstable_features,
+    unused_import_braces
+)]
+
+pub mod gva_conf;
+
+use crate::gva_conf::GvaConf;
+use dubp::crypto::keys::ed25519::Ed25519KeyPair;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug)]
+pub struct DuniterConf {
+    pub gva: Option<GvaConf>,
+    pub self_key_pair: Ed25519KeyPair,
+    pub txs_mempool_size: usize,
+}
+
+impl Default for DuniterConf {
+    fn default() -> Self {
+        DuniterConf {
+            gva: None,
+            self_key_pair: Ed25519KeyPair::generate_random().expect("fail to gen random keypair"),
+            txs_mempool_size: 0,
+        }
+    }
+}
diff --git a/rust-libs/duniter-dbs/src/cm_v1.rs b/rust-libs/duniter-dbs/src/cm_v1.rs
index 7e1eb4e1e..f94dc2176 100644
--- a/rust-libs/duniter-dbs/src/cm_v1.rs
+++ b/rust-libs/duniter-dbs/src/cm_v1.rs
@@ -19,6 +19,6 @@ db_schema!(
     CmV1,
     [
         //["self_pubkey", self_pubkey, (), PubKeyValV2,],
-        ["self_peer_card", SelfPeerCard, (), PeerCardDbV1],
+        ["self_peer_old", SelfPeerOld, (), PeerCardDbV1],
     ]
 );
diff --git a/rust-libs/duniter-dbs/src/lib.rs b/rust-libs/duniter-dbs/src/lib.rs
index b43c04d56..b6c88e9a4 100644
--- a/rust-libs/duniter-dbs/src/lib.rs
+++ b/rust-libs/duniter-dbs/src/lib.rs
@@ -131,3 +131,17 @@ pub struct DuniterDbs {
     pub gva_db: GvaV1Db<DbsBackend>,
     pub txs_mp_db: TxsMpV2Db<DbsBackend>,
 }
+
+#[cfg(feature = "mem")]
+impl DuniterDbs {
+    pub fn mem() -> KvResult<Self> {
+        use bc_v2::BcV2DbWritable as _;
+        use cm_v1::CmV1DbWritable as _;
+        Ok(DuniterDbs {
+            bc_db: bc_v2::BcV2Db::<Mem>::open(MemConf::default())?,
+            cm_db: cm_v1::CmV1Db::<MemSingleton>::open(MemSingletonConf::default())?,
+            gva_db: GvaV1Db::<Mem>::open(MemConf::default())?,
+            txs_mp_db: TxsMpV2Db::<Mem>::open(MemConf::default())?,
+        })
+    }
+}
diff --git a/rust-libs/duniter-gva/src/lib.rs b/rust-libs/duniter-gva/src/lib.rs
deleted file mode 100644
index 7d542244d..000000000
--- a/rust-libs/duniter-gva/src/lib.rs
+++ /dev/null
@@ -1,253 +0,0 @@
-//  Copyright (C) 2020 Éloïs SANCHEZ.
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, either version 3 of the
-// License, or (at your option) any later version.
-//
-// 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-#![deny(
-    clippy::unwrap_used,
-    missing_copy_implementations,
-    trivial_casts,
-    trivial_numeric_casts,
-    unstable_features,
-    unused_import_braces
-)]
-
-mod entities;
-mod mutations;
-mod queries;
-mod schema;
-mod subscriptions;
-mod warp_;
-
-use crate::entities::{
-    tx_gva::TxGva,
-    ud_gva::{CurrentUdGva, RevalUdGva, UdGva},
-    TxsHistoryGva, UtxoGva,
-};
-use crate::schema::{GraphQlSchema, SchemaData};
-use async_graphql::http::GraphQLPlaygroundConfig;
-use dubp::common::crypto::keys::{ed25519::PublicKey, PublicKey as _};
-use dubp::common::prelude::*;
-use dubp::documents::prelude::*;
-use dubp::documents::transaction::{
-    TransactionDocumentTrait, TransactionDocumentV10, TransactionDocumentV10Builder,
-    TransactionInputUnlocksV10, TransactionInputV10, TransactionOutputV10, UTXOConditions,
-};
-use dubp::documents_parser::prelude::*;
-use dubp::wallet::prelude::*;
-use duniter_dbs::prelude::*;
-use duniter_dbs::{kv_typed::prelude::*, TxDbV2, TxsMpV2DbReadable};
-use duniter_mempools::TxsMempool;
-use futures::{StreamExt, TryStreamExt};
-use resiter::map::Map;
-use smallvec::smallvec as svec;
-use std::convert::Infallible;
-use std::ops::Deref;
-use warp::{http::Response as HttpResponse, Filter as _, Rejection, Stream};
-
-#[derive(Clone, Debug)]
-pub struct GvaConf {
-    host: String,
-    port: u16,
-    path: String,
-    subscriptions_path: String,
-}
-
-impl Default for GvaConf {
-    fn default() -> Self {
-        GvaConf {
-            host: "localhost".to_owned(),
-            port: 30901,
-            path: "gva".to_owned(),
-            subscriptions_path: "gva-sub".to_owned(),
-        }
-    }
-}
-
-impl GvaConf {
-    pub fn host(&mut self, host: String) {
-        self.host = host;
-    }
-    pub fn port(&mut self, port: u16) {
-        self.port = port;
-    }
-    pub fn path(&mut self, mut path: String) {
-        if path.starts_with('/') {
-            path.remove(0);
-            self.path = path;
-        } else {
-            self.path = path;
-        }
-    }
-    pub fn subscriptions_path(&mut self, mut subscriptions_path: String) {
-        if subscriptions_path.starts_with('/') {
-            subscriptions_path.remove(0);
-            self.subscriptions_path = subscriptions_path;
-        } else {
-            self.subscriptions_path = subscriptions_path;
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug)]
-pub struct GvaServer;
-
-#[derive(Debug, Default)]
-pub struct ServerMetaData {
-    pub currency: String,
-    pub self_pubkey: PublicKey,
-    pub software_version: &'static str,
-}
-
-impl GvaServer {
-    pub fn start(
-        conf: GvaConf,
-        dbs: DuniterDbs,
-        dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
-        server_meta_data: ServerMetaData,
-        txs_mempool: TxsMempool,
-    ) -> Result<(), tokio::io::Error> {
-        println!("TMP GvaServer::start: conf={:?}", conf);
-        let mut runtime = tokio::runtime::Builder::new()
-            .threaded_scheduler()
-            .enable_all()
-            .build()?;
-        std::thread::spawn(move || {
-            runtime.block_on(async {
-                let schema = async_graphql::Schema::build(
-                    queries::QueryRoot::default(),
-                    mutations::MutationRoot::default(),
-                    subscriptions::SubscriptionRoot::default(),
-                )
-                .data(schema::SchemaData {
-                    dbs,
-                    dbs_pool,
-                    server_meta_data,
-                    txs_mempool,
-                })
-                .extension(async_graphql::extensions::Logger)
-                .finish();
-
-                let graphql_post = warp_::graphql(
-                    &conf,
-                    schema.clone(),
-                    async_graphql::http::MultipartOptions::default(),
-                );
-
-                let conf_clone = conf.clone();
-                let graphql_playground =
-                    warp::path::path(conf.path.clone())
-                        .and(warp::get())
-                        .map(move || {
-                            HttpResponse::builder()
-                                .header("content-type", "text/html")
-                                .body(async_graphql::http::playground_source(
-                                    GraphQLPlaygroundConfig::new(&format!("/{}", &conf_clone.path))
-                                        .subscription_endpoint(&format!(
-                                            "/{}",
-                                            &conf_clone.subscriptions_path,
-                                        )),
-                                ))
-                        });
-
-                let routes = graphql_playground
-                    .or(graphql_post)
-                    .or(warp_::graphql_ws(&conf, schema.clone()))
-                    .recover(|err: Rejection| async move {
-                        if let Some(warp_::BadRequest(err)) = err.find() {
-                            return Ok::<_, Infallible>(warp::reply::with_status(
-                                err.to_string(),
-                                http::StatusCode::BAD_REQUEST,
-                            ));
-                        }
-
-                        Ok(warp::reply::with_status(
-                            "INTERNAL_SERVER_ERROR".to_string(),
-                            http::StatusCode::INTERNAL_SERVER_ERROR,
-                        ))
-                    });
-
-                log::info!(
-                    "Start GVA server at http://localhost:{}/{}",
-                    conf.port,
-                    &conf.path
-                );
-                warp::serve(routes).run(([0, 0, 0, 0], conf.port)).await;
-            });
-            log::warn!("GVA server stopped");
-        });
-        Ok(())
-    }
-}
-
-#[derive(
-    async_graphql::SimpleObject, Clone, Debug, Default, serde::Deserialize, serde::Serialize,
-)]
-#[serde(rename_all = "camelCase")]
-#[graphql(name = "PeerCard")]
-pub struct PeerCardStringified {
-    pub version: u32,
-    pub currency: String,
-    pub pubkey: String,
-    pub blockstamp: String,
-    pub endpoints: Vec<String>,
-    pub status: String,
-    pub signature: String,
-}
-impl From<duniter_dbs::PeerCardDbV1> for PeerCardStringified {
-    fn from(peer: duniter_dbs::PeerCardDbV1) -> Self {
-        Self {
-            version: peer.version,
-            currency: peer.currency,
-            pubkey: peer.pubkey,
-            blockstamp: peer.blockstamp,
-            endpoints: peer.endpoints,
-            status: peer.status,
-            signature: peer.signature,
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use duniter_dbs::bc_v2::{BcV2Db, BcV2DbWritable};
-    use duniter_dbs::cm_v1::{CmV1Db, CmV1DbWritable};
-    use duniter_dbs::kv_typed::backend::memory::{Mem, MemConf};
-    use duniter_dbs::{GvaV1Db, GvaV1DbWritable, TxsMpV2Db, TxsMpV2DbWritable};
-    use fast_threadpool::ThreadPoolConfig;
-    use unwrap::unwrap;
-
-    #[test]
-    #[ignore]
-    fn launch_mem_gva() {
-        let dbs = DuniterDbs {
-            bc_db: unwrap!(BcV2Db::<Mem>::open(MemConf::default())),
-            cm_db: unwrap!(CmV1Db::<MemSingleton>::open(MemSingletonConf::default())),
-            gva_db: unwrap!(GvaV1Db::<Mem>::open(MemConf::default())),
-            txs_mp_db: unwrap!(TxsMpV2Db::<Mem>::open(MemConf::default())),
-        };
-        let threadpool =
-            fast_threadpool::ThreadPool::start(ThreadPoolConfig::default(), dbs.clone());
-
-        unwrap!(GvaServer::start(
-            GvaConf::default(),
-            dbs,
-            threadpool.into_async_handler(),
-            ServerMetaData::default(),
-            TxsMempool::new(10)
-        ));
-
-        std::thread::sleep(std::time::Duration::from_secs(120));
-    }
-}
diff --git a/rust-libs/duniter-mempools/src/lib.rs b/rust-libs/duniter-mempools/src/lib.rs
index c132ff116..9ebbc0f8c 100644
--- a/rust-libs/duniter-mempools/src/lib.rs
+++ b/rust-libs/duniter-mempools/src/lib.rs
@@ -31,6 +31,11 @@ use duniter_dbs::kv_typed::prelude::*;
 use duniter_dbs::{GvaV1DbReadable, TxsMpV2Db, TxsMpV2DbReadable};
 use thiserror::Error;
 
+#[derive(Clone, Copy, Debug, Default)]
+pub struct Mempools {
+    pub txs: TxsMempool,
+}
+
 #[derive(Debug, Error)]
 pub enum TxMpError {
     #[error("{0}")]
@@ -47,7 +52,7 @@ impl From<KvError> for TxMpError {
     }
 }
 
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Default)]
 pub struct TxsMempool {
     max_size: usize,
 }
diff --git a/rust-libs/duniter-module/Cargo.toml b/rust-libs/duniter-module/Cargo.toml
new file mode 100644
index 000000000..8d2b55504
--- /dev/null
+++ b/rust-libs/duniter-module/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "duniter-module"
+version = "0.1.0"
+authors = ["librelois <elois@duniter.org>"]
+license = "AGPL-3.0"
+edition = "2018"
+
+[dependencies]
+anyhow = "1.0.34"
+async-trait = "0.1.41"
+dubp = { version = "0.30.0" }
+duniter-conf = { path = "../duniter-conf" }
+duniter-dbs = { path = "../duniter-dbs" }
+duniter-mempools = { path = "../duniter-mempools" }
+fast-threadpool = "0.2.1"
+
+[dev-dependencies]
+duniter-dbs = { path = "../duniter-dbs", features = ["mem"] }
+paste = "1.0.2"
+tokio = { version = "0.2.22", features = ["macros", "rt-core"] }
diff --git a/rust-libs/duniter-module/src/lib.rs b/rust-libs/duniter-module/src/lib.rs
new file mode 100644
index 000000000..28f49c53b
--- /dev/null
+++ b/rust-libs/duniter-module/src/lib.rs
@@ -0,0 +1,165 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+#![deny(
+    clippy::unwrap_used,
+    missing_copy_implementations,
+    trivial_casts,
+    trivial_numeric_casts,
+    unstable_features,
+    unused_import_braces
+)]
+
+use duniter_conf::DuniterConf;
+use duniter_dbs::DuniterDbs;
+use duniter_mempools::Mempools;
+use std::path::Path;
+
+pub type Endpoint = String;
+
+#[async_trait::async_trait]
+pub trait DuniterModule: 'static + Sized {
+    fn init(
+        conf: &DuniterConf,
+        currency: &str,
+        dbs_pool: &fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
+        mempools: Mempools,
+        profile_path_opt: Option<&Path>,
+        software_version: &'static str,
+    ) -> anyhow::Result<(Self, Vec<Endpoint>)>;
+
+    async fn start(self) -> anyhow::Result<()>;
+}
+
+#[macro_export]
+macro_rules! plug_duniter_modules {
+    ([$($M:ty),*]) => {
+        paste::paste! {
+            use anyhow::Context as _;
+            async fn start_duniter_modules(
+                conf: &DuniterConf,
+                currency: String,
+                dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
+                mempools: duniter_mempools::Mempools,
+                profile_path_opt: Option<std::path::PathBuf>,
+                software_version: &'static str,
+            ) -> anyhow::Result<()> {
+                let mut all_endpoints = Vec::<String>::new();
+                $(
+                    let ([<$M:snake>], mut endpoints) =<$M>::init(conf, &currency, &dbs_pool, mempools, profile_path_opt.as_deref(), software_version)
+                        .with_context(|| format!("Fail to init module '{}'", stringify!($M)))?;
+                    all_endpoints.append(&mut endpoints);
+                )*
+
+                let self_peer = duniter_dbs::PeerCardDbV1 {
+                    version: 10,
+                    currency,
+                    endpoints: all_endpoints,
+                    ..Default::default()
+                };
+
+                use duniter_dbs::cm_v1::CmV1DbWritable as _;
+                use duniter_dbs::kv_typed::prelude::DbCollectionRw as _;
+                dbs_pool.execute(|dbs| dbs.cm_db.self_peer_old_write().upsert((), self_peer)).await?.context("fail to save self peer card")?;
+
+                $(
+                    let [<$M:snake _handle>] = tokio::spawn([<$M:snake>].start());
+                )*
+
+                $(
+                    [<$M:snake _handle>].await.map_err(|e| if e.is_cancelled() {
+                        anyhow::Error::msg(format!("Module '{}' cancelled", stringify!($M)))
+                    } else {
+                        anyhow::Error::msg(format!("Module '{}' panic", stringify!($M)))
+                    })?
+                    .with_context(|| format!("Error on execution of module '{}'", stringify!($M)))?;
+                )*
+
+                Ok(())
+            }
+        }
+    };
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use duniter_mempools::TxsMempool;
+
+    struct TestMod1;
+
+    #[async_trait::async_trait]
+    impl DuniterModule for TestMod1 {
+        fn init(
+            _conf: &DuniterConf,
+            _currency: &str,
+            _dbs_pool: &fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
+            _mempools: Mempools,
+            profile_path_opt: Option<&Path>,
+            _software_version: &'static str,
+        ) -> anyhow::Result<(Self, Vec<Endpoint>)> {
+            if let Some(profile_path) = profile_path_opt {
+                let _file_path = profile_path.join("test_mod1.json");
+            }
+            Ok((TestMod1, vec![]))
+        }
+
+        async fn start(self) -> anyhow::Result<()> {
+            Ok(())
+        }
+    }
+
+    struct TestMod2;
+
+    #[async_trait::async_trait]
+    impl DuniterModule for TestMod2 {
+        fn init(
+            _conf: &DuniterConf,
+            _currency: &str,
+            _dbs_pool: &fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
+            _mempools: Mempools,
+            _profile_path_opt: Option<&Path>,
+            _software_version: &'static str,
+        ) -> anyhow::Result<(Self, Vec<Endpoint>)> {
+            Ok((TestMod2, vec![]))
+        }
+
+        async fn start(self) -> anyhow::Result<()> {
+            Ok(())
+        }
+    }
+
+    #[tokio::test]
+    async fn test_macro_plug_duniter_modules() -> anyhow::Result<()> {
+        plug_duniter_modules!([TestMod1, TestMod2]);
+
+        let dbs = DuniterDbs::mem()?;
+        let threadpool =
+            fast_threadpool::ThreadPool::start(fast_threadpool::ThreadPoolConfig::default(), dbs);
+
+        start_duniter_modules(
+            &DuniterConf::default(),
+            "test".to_owned(),
+            threadpool.into_async_handler(),
+            Mempools {
+                txs: TxsMempool::new(0),
+            },
+            None,
+            "",
+        )
+        .await?;
+        Ok(())
+    }
+}
diff --git a/rust-libs/duniter-server/Cargo.toml b/rust-libs/duniter-server/Cargo.toml
index a33c8b376..ff7017c66 100644
--- a/rust-libs/duniter-server/Cargo.toml
+++ b/rust-libs/duniter-server/Cargo.toml
@@ -6,16 +6,21 @@ license = "AGPL-3.0"
 edition = "2018"
 
 [dependencies]
+anyhow = "1.0.34"
 dubp = { version = "0.30.0" }
+duniter-conf = { path = "../duniter-conf" }
 duniter-dbs = { path = "../duniter-dbs" }
 duniter-dbs-read-ops = { path = "../duniter-dbs-read-ops" }
 duniter-dbs-write-ops = { path = "../duniter-dbs-write-ops" }
-duniter-gva = { path = "../duniter-gva" }
+duniter-gva = { path = "../modules/duniter-gva" }
 duniter-mempools = { path = "../duniter-mempools" }
+duniter-module = { path = "../duniter-module" }
 fast-threadpool = "0.2.1"
 flume = "0.9.1"
 log = "0.4.11"
+paste = "1.0.2"
 resiter = "0.4.0"
+tokio = { version = "0.2.22", features = ["io-util", "rt-threaded"] }
 
 [dev-dependencies]
 unwrap = "1.2.1"
diff --git a/rust-libs/duniter-server/src/lib.rs b/rust-libs/duniter-server/src/lib.rs
index 77bdad24b..c1cf343d7 100644
--- a/rust-libs/duniter-server/src/lib.rs
+++ b/rust-libs/duniter-server/src/lib.rs
@@ -22,25 +22,26 @@
     unused_import_braces
 )]
 
+pub use duniter_conf::DuniterConf;
 pub use duniter_dbs::smallvec;
-use duniter_gva::ServerMetaData;
-use duniter_mempools::{TxMpError, TxsMempool};
-use fast_threadpool::ThreadPoolConfig;
-
-pub use duniter_gva::{GvaConf, PeerCardStringified};
+pub use duniter_gva::{GvaConf, GvaModule, PeerCardStringified};
 
+use anyhow::Context;
 use dubp::common::crypto::keys::ed25519::PublicKey;
 use dubp::common::prelude::*;
 use dubp::documents::{prelude::*, transaction::TransactionDocumentV10};
 use dubp::{
     block::prelude::*, common::crypto::hashs::Hash, documents_parser::prelude::FromStringObject,
 };
-use duniter_dbs::cm_v1::CmV1DbWritable;
+use duniter_dbs::cm_v1::{CmV1DbReadable, CmV1DbWritable};
 use duniter_dbs::{
     kv_typed::prelude::*, GvaV1DbReadable, HashKeyV2, PendingTxDbV2, TxsMpV2DbReadable,
 };
 use duniter_dbs::{prelude::*, BlockMetaV2};
 use duniter_dbs_read_ops::txs_history::TxsHistory;
+use duniter_mempools::{Mempools, TxMpError, TxsMempool};
+use duniter_module::{plug_duniter_modules, DuniterModule as _, Endpoint};
+use fast_threadpool::ThreadPoolConfig;
 use resiter::filter::Filter;
 use std::{collections::BTreeMap, path::Path};
 
@@ -50,29 +51,24 @@ pub enum DuniterCommand {
     Start,
 }
 
-#[derive(Clone, Debug)]
-pub struct DuniterServerConf {
-    pub gva: Option<GvaConf>,
-    pub self_pubkey: PublicKey,
-    pub txs_mempool_size: usize,
-}
-
 pub struct DuniterServer {
-    conf: DuniterServerConf,
+    conf: DuniterConf,
     current: Option<BlockMetaV2>,
     dbs_pool: fast_threadpool::ThreadPoolSyncHandler<DuniterDbs>,
     pending_txs_subscriber: flume::Receiver<Arc<Events<duniter_dbs::txs_mp_v2::TxsEvent>>>,
     txs_mempool: TxsMempool,
 }
 
+plug_duniter_modules!([GvaModule]);
+
 impl DuniterServer {
     pub fn start(
         command_name: Option<String>,
-        conf: DuniterServerConf,
+        conf: DuniterConf,
         currency: String,
         home_path_opt: Option<&Path>,
         software_version: &'static str,
-    ) -> Self {
+    ) -> anyhow::Result<Self> {
         let command = match command_name.unwrap_or_default().as_str() {
             "sync" => DuniterCommand::Sync,
             _ => DuniterCommand::Start,
@@ -83,8 +79,8 @@ impl DuniterServer {
         log::info!("open duniter databases...");
         let dbs = duniter_dbs::open_dbs(home_path_opt);
         log::info!("Databases successfully opened.");
-        let current =
-            duniter_dbs_read_ops::get_current_block_meta(&dbs.bc_db).expect("Fail to get current");
+        let current = duniter_dbs_read_ops::get_current_block_meta(&dbs.bc_db)
+            .context("Fail to get current")?;
         if let Some(current) = current {
             log::info!("Current block: #{}-{}", current.number, current.hash);
         } else {
@@ -95,46 +91,60 @@ impl DuniterServer {
         dbs.txs_mp_db
             .txs()
             .subscribe(s)
-            .expect("Fail to subscribe to txs col");
+            .context("Fail to subscribe to txs col")?;
 
         let threadpool = if home_path_opt.is_some() {
             log::info!("start dbs threadpool...");
-            let threadpool =
-                fast_threadpool::ThreadPool::start(ThreadPoolConfig::default(), dbs.clone());
+            let threadpool = fast_threadpool::ThreadPool::start(ThreadPoolConfig::default(), dbs);
 
-            if command != DuniterCommand::Sync {
-                if let Some(gva_conf) = conf.gva.clone() {
-                    duniter_gva::GvaServer::start(
-                        gva_conf,
-                        dbs,
-                        threadpool.async_handler(),
-                        ServerMetaData {
+            if command != DuniterCommand::Sync && conf.gva.is_some() {
+                let mut runtime = tokio::runtime::Builder::new()
+                    .threaded_scheduler()
+                    .enable_all()
+                    .build()?;
+                let conf_clone = conf.clone();
+                let threadpool_async_handler = threadpool.async_handler();
+                std::thread::spawn(move || {
+                    runtime
+                        .block_on(start_duniter_modules(
+                            &conf_clone,
                             currency,
-                            self_pubkey: conf.self_pubkey,
+                            threadpool_async_handler,
+                            Mempools { txs: txs_mempool },
+                            None,
                             software_version,
-                        },
-                        txs_mempool,
-                    )
-                    .expect("Fail to start GVA server");
-                }
+                        ))
+                        .context("Fail to start duniter modules")
+                });
             }
             threadpool
         } else {
             fast_threadpool::ThreadPool::start(ThreadPoolConfig::low(), dbs)
         };
 
-        DuniterServer {
+        Ok(DuniterServer {
             conf,
             current,
             dbs_pool: threadpool.into_sync_handler(),
             pending_txs_subscriber,
             txs_mempool,
-        }
+        })
     }
 
     /*
      * READ FUNCTIONS FOR DUNITER JS ONLY
      */
+    pub fn get_self_endpoints(&self) -> anyhow::Result<Vec<Endpoint>> {
+        if let Some(self_peer) = self
+            .dbs_pool
+            .execute(|dbs| dbs.cm_db.self_peer_old().get(&()))?
+            .context("fail to get self endpoints")?
+        {
+            Ok(self_peer.endpoints)
+        } else {
+            Ok(vec![])
+        }
+    }
     pub fn accept_new_tx(
         &self,
         tx: TransactionDocumentV10,
@@ -305,7 +315,7 @@ impl DuniterServer {
         self.dbs_pool
             .execute(move |dbs| {
                 dbs.cm_db
-                    .self_peer_card_write()
+                    .self_peer_old_write()
                     .upsert(
                         (),
                         duniter_dbs::PeerCardDbV1 {
@@ -327,22 +337,23 @@ impl DuniterServer {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use dubp::documents::smallvec::smallvec;
     use dubp::documents::transaction::TransactionDocumentV10Builder;
+    use dubp::{crypto::keys::ed25519::Ed25519KeyPair, documents::smallvec::smallvec};
 
     #[test]
-    fn test_txs_history() -> KvResult<()> {
+    fn test_txs_history() -> anyhow::Result<()> {
         let server = DuniterServer::start(
             None,
-            DuniterServerConf {
+            DuniterConf {
                 gva: None,
-                self_pubkey: PublicKey::default(),
+                self_key_pair: Ed25519KeyPair::generate_random()
+                    .expect("fail to gen random keypair"),
                 txs_mempool_size: 200,
             },
             "currency_test".to_owned(),
             None,
             "test",
-        );
+        )?;
 
         let tx = TransactionDocumentV10Builder {
             currency: "duniter_unit_test_currency",
diff --git a/rust-libs/duniter-gva/Cargo.toml b/rust-libs/modules/duniter-gva/Cargo.toml
similarity index 58%
rename from rust-libs/duniter-gva/Cargo.toml
rename to rust-libs/modules/duniter-gva/Cargo.toml
index 93c56d409..d4e502c50 100644
--- a/rust-libs/duniter-gva/Cargo.toml
+++ b/rust-libs/modules/duniter-gva/Cargo.toml
@@ -8,10 +8,13 @@ edition = "2018"
 [dependencies]
 anyhow = "1.0.33"
 async-graphql = "2.0.0"
+async-trait = "0.1.41"
 dubp = { version = "0.30.0" }
-duniter-dbs = { path = "../duniter-dbs" }
-duniter-dbs-read-ops = { path = "../duniter-dbs-read-ops" }
-duniter-mempools = { path = "../duniter-mempools" }
+duniter-conf = { path = "../../duniter-conf" }
+duniter-dbs = { path = "../../duniter-dbs" }
+duniter-dbs-read-ops = { path = "../../duniter-dbs-read-ops" }
+duniter-mempools = { path = "../../duniter-mempools" }
+duniter-module = { path = "../../duniter-module" }
 fast-threadpool = "0.2.1"
 flume = "0.9.1"
 futures = "0.3.6"
@@ -21,9 +24,10 @@ resiter = "0.4.0"
 serde = { version = "1.0.105", features = ["derive"] }
 serde_urlencoded = "0.7.0"
 smallvec = { version = "1.4.0", features = ["serde", "write"] }
-tokio = { version = "0.2.22", features = ["io-util", "rt-threaded", "stream"] }
+tokio = { version = "0.2.22", features = ["io-util", "rt-threaded"] }
 warp = "0.2"
 
 [dev-dependencies]
-duniter-dbs = { path = "../duniter-dbs", features = ["mem"] }
+duniter-dbs = { path = "../../duniter-dbs", features = ["mem"] }
+tokio = { version = "0.2.22", features = ["macros", "rt-threaded"] }
 unwrap = "1.2.1"
diff --git a/rust-libs/duniter-gva/src/entities.rs b/rust-libs/modules/duniter-gva/src/entities.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/entities.rs
rename to rust-libs/modules/duniter-gva/src/entities.rs
diff --git a/rust-libs/duniter-gva/src/entities/tx_gva.rs b/rust-libs/modules/duniter-gva/src/entities/tx_gva.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/entities/tx_gva.rs
rename to rust-libs/modules/duniter-gva/src/entities/tx_gva.rs
diff --git a/rust-libs/duniter-gva/src/entities/ud_gva.rs b/rust-libs/modules/duniter-gva/src/entities/ud_gva.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/entities/ud_gva.rs
rename to rust-libs/modules/duniter-gva/src/entities/ud_gva.rs
diff --git a/rust-libs/duniter-gva/src/inputs_validators.rs b/rust-libs/modules/duniter-gva/src/inputs_validators.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/inputs_validators.rs
rename to rust-libs/modules/duniter-gva/src/inputs_validators.rs
diff --git a/rust-libs/modules/duniter-gva/src/lib.rs b/rust-libs/modules/duniter-gva/src/lib.rs
new file mode 100644
index 000000000..e680ee616
--- /dev/null
+++ b/rust-libs/modules/duniter-gva/src/lib.rs
@@ -0,0 +1,287 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+#![deny(
+    clippy::unwrap_used,
+    missing_copy_implementations,
+    trivial_casts,
+    trivial_numeric_casts,
+    unstable_features,
+    unused_import_braces
+)]
+
+pub use duniter_conf::gva_conf::GvaConf;
+
+mod entities;
+mod mutations;
+mod queries;
+mod schema;
+mod subscriptions;
+mod warp_;
+
+use crate::entities::{
+    tx_gva::TxGva,
+    ud_gva::{CurrentUdGva, RevalUdGva, UdGva},
+    TxsHistoryGva, UtxoGva,
+};
+use crate::schema::{GraphQlSchema, SchemaData};
+use async_graphql::http::GraphQLPlaygroundConfig;
+use dubp::common::crypto::keys::{ed25519::PublicKey, KeyPair as _, PublicKey as _};
+use dubp::common::prelude::*;
+use dubp::documents::prelude::*;
+use dubp::documents::transaction::{
+    TransactionDocumentTrait, TransactionDocumentV10, TransactionDocumentV10Builder,
+    TransactionInputUnlocksV10, TransactionInputV10, TransactionOutputV10, UTXOConditions,
+};
+use dubp::documents_parser::prelude::*;
+use dubp::wallet::prelude::*;
+use duniter_dbs::prelude::*;
+use duniter_dbs::{kv_typed::prelude::*, TxDbV2, TxsMpV2DbReadable};
+use duniter_mempools::{Mempools, TxsMempool};
+use futures::{StreamExt, TryStreamExt};
+use resiter::map::Map;
+use smallvec::smallvec as svec;
+use std::convert::Infallible;
+use std::ops::Deref;
+use warp::{http::Response as HttpResponse, Filter as _, Rejection, Stream};
+
+#[derive(Debug)]
+pub struct GvaModule {
+    conf: Option<GvaConf>,
+    currency: String,
+    dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
+    mempools: Mempools,
+    self_pubkey: PublicKey,
+    software_version: &'static str,
+}
+
+#[async_trait::async_trait]
+impl duniter_module::DuniterModule for GvaModule {
+    fn init(
+        conf: &duniter_conf::DuniterConf,
+        currency: &str,
+        dbs_pool: &fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
+        mempools: Mempools,
+        _profile_path_opt: Option<&std::path::Path>,
+        software_version: &'static str,
+    ) -> anyhow::Result<(Self, Vec<duniter_module::Endpoint>)> {
+        let mut endpoints = Vec::new();
+        if let Some(conf) = conf.gva.clone() {
+            let remote_port = conf.get_remote_port();
+            endpoints.push(format!(
+                "GVA {}{} {} {}",
+                if remote_port == 443 || conf.get_remote_tls() {
+                    "S "
+                } else {
+                    ""
+                },
+                conf.get_remote_host(),
+                remote_port,
+                conf.get_remote_path(),
+            ));
+            endpoints.push(format!(
+                "GVASUB {}{} {} {}",
+                if remote_port == 443 || conf.get_remote_tls() {
+                    "S "
+                } else {
+                    ""
+                },
+                conf.get_remote_host(),
+                remote_port,
+                conf.get_remote_subscriptions_path(),
+            ));
+        };
+        Ok((
+            GvaModule {
+                conf: conf.gva.to_owned(),
+                currency: currency.to_owned(),
+                dbs_pool: dbs_pool.to_owned(),
+                mempools,
+                self_pubkey: conf.self_key_pair.public_key(),
+                software_version,
+            },
+            endpoints,
+        ))
+    }
+
+    async fn start(self) -> anyhow::Result<()> {
+        let GvaModule {
+            conf,
+            currency,
+            dbs_pool,
+            mempools,
+            self_pubkey,
+            software_version,
+        } = self;
+
+        if let Some(conf) = conf {
+            GvaModule::start_inner(
+                conf,
+                currency,
+                dbs_pool,
+                mempools,
+                self_pubkey,
+                software_version,
+            )
+            .await
+        }
+        Ok(())
+    }
+}
+
+impl GvaModule {
+    async fn start_inner(
+        conf: GvaConf,
+        currency: String,
+        dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
+        mempools: Mempools,
+        self_pubkey: PublicKey,
+        software_version: &'static str,
+    ) {
+        log::info!("GvaServer::start: conf={:?}", conf);
+        let schema = async_graphql::Schema::build(
+            queries::QueryRoot::default(),
+            mutations::MutationRoot::default(),
+            subscriptions::SubscriptionRoot::default(),
+        )
+        .data(schema::SchemaData {
+            dbs_pool,
+            server_meta_data: ServerMetaData {
+                currency,
+                self_pubkey,
+                software_version,
+            },
+            txs_mempool: mempools.txs,
+        })
+        .extension(async_graphql::extensions::Logger)
+        .finish();
+
+        let graphql_post = warp_::graphql(
+            &conf,
+            schema.clone(),
+            async_graphql::http::MultipartOptions::default(),
+        );
+
+        let conf_clone = conf.clone();
+        let graphql_playground =
+            warp::path::path(conf.get_path())
+                .and(warp::get())
+                .map(move || {
+                    HttpResponse::builder()
+                        .header("content-type", "text/html")
+                        .body(async_graphql::http::playground_source(
+                            GraphQLPlaygroundConfig::new(&format!("/{}", &conf_clone.get_path()))
+                                .subscription_endpoint(&format!(
+                                    "/{}",
+                                    &conf_clone.get_subscriptions_path(),
+                                )),
+                        ))
+                });
+
+        let routes = graphql_playground
+            .or(graphql_post)
+            .or(warp_::graphql_ws(&conf, schema.clone()))
+            .recover(|err: Rejection| async move {
+                if let Some(warp_::BadRequest(err)) = err.find() {
+                    return Ok::<_, Infallible>(warp::reply::with_status(
+                        err.to_string(),
+                        http::StatusCode::BAD_REQUEST,
+                    ));
+                }
+
+                Ok(warp::reply::with_status(
+                    "INTERNAL_SERVER_ERROR".to_string(),
+                    http::StatusCode::INTERNAL_SERVER_ERROR,
+                ))
+            });
+
+        log::info!(
+            "GVA server listen on http://{}:{}/{}",
+            &conf.get_host(),
+            conf.get_port(),
+            &conf.get_path()
+        );
+        warp::serve(routes)
+            .run(([0, 0, 0, 0], conf.get_port()))
+            .await;
+        log::warn!("GVA server stopped");
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct ServerMetaData {
+    pub currency: String,
+    pub self_pubkey: PublicKey,
+    pub software_version: &'static str,
+}
+
+#[derive(
+    async_graphql::SimpleObject, Clone, Debug, Default, serde::Deserialize, serde::Serialize,
+)]
+#[serde(rename_all = "camelCase")]
+#[graphql(name = "PeerCard")]
+pub struct PeerCardStringified {
+    pub version: u32,
+    pub currency: String,
+    pub pubkey: String,
+    pub blockstamp: String,
+    pub endpoints: Vec<String>,
+    pub status: String,
+    pub signature: String,
+}
+impl From<duniter_dbs::PeerCardDbV1> for PeerCardStringified {
+    fn from(peer: duniter_dbs::PeerCardDbV1) -> Self {
+        Self {
+            version: peer.version,
+            currency: peer.currency,
+            pubkey: peer.pubkey,
+            blockstamp: peer.blockstamp,
+            endpoints: peer.endpoints,
+            status: peer.status,
+            signature: peer.signature,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use duniter_conf::DuniterConf;
+    use duniter_mempools::Mempools;
+    use duniter_module::DuniterModule;
+    use fast_threadpool::ThreadPoolConfig;
+    use unwrap::unwrap;
+
+    #[tokio::test]
+    #[ignore]
+    async fn launch_mem_gva() -> anyhow::Result<()> {
+        let dbs = unwrap!(DuniterDbs::mem());
+        let threadpool = fast_threadpool::ThreadPool::start(ThreadPoolConfig::default(), dbs);
+
+        GvaModule::init(
+            &DuniterConf::default(),
+            "",
+            &threadpool.into_async_handler(),
+            Mempools::default(),
+            None,
+            "test",
+        )?
+        .0
+        .start()
+        .await?;
+
+        Ok(())
+    }
+}
diff --git a/rust-libs/duniter-gva/src/mutations.rs b/rust-libs/modules/duniter-gva/src/mutations.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/mutations.rs
rename to rust-libs/modules/duniter-gva/src/mutations.rs
diff --git a/rust-libs/duniter-gva/src/queries.rs b/rust-libs/modules/duniter-gva/src/queries.rs
similarity index 96%
rename from rust-libs/duniter-gva/src/queries.rs
rename to rust-libs/modules/duniter-gva/src/queries.rs
index 744263987..11d8e29c5 100644
--- a/rust-libs/duniter-gva/src/queries.rs
+++ b/rust-libs/modules/duniter-gva/src/queries.rs
@@ -49,7 +49,7 @@ impl Node {
 
         Ok(data
             .dbs_pool
-            .execute(move |dbs| dbs.cm_db.self_peer_card().get(&()))
+            .execute(move |dbs| dbs.cm_db.self_peer_old().get(&()))
             .await??
             .map(Into::into))
     }
diff --git a/rust-libs/duniter-gva/src/queries/gen_txs.rs b/rust-libs/modules/duniter-gva/src/queries/gen_txs.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/queries/gen_txs.rs
rename to rust-libs/modules/duniter-gva/src/queries/gen_txs.rs
diff --git a/rust-libs/duniter-gva/src/queries/txs_history.rs b/rust-libs/modules/duniter-gva/src/queries/txs_history.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/queries/txs_history.rs
rename to rust-libs/modules/duniter-gva/src/queries/txs_history.rs
diff --git a/rust-libs/duniter-gva/src/queries/uds.rs b/rust-libs/modules/duniter-gva/src/queries/uds.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/queries/uds.rs
rename to rust-libs/modules/duniter-gva/src/queries/uds.rs
diff --git a/rust-libs/duniter-gva/src/queries/utxos.rs b/rust-libs/modules/duniter-gva/src/queries/utxos.rs
similarity index 100%
rename from rust-libs/duniter-gva/src/queries/utxos.rs
rename to rust-libs/modules/duniter-gva/src/queries/utxos.rs
diff --git a/rust-libs/duniter-gva/src/schema.rs b/rust-libs/modules/duniter-gva/src/schema.rs
similarity index 97%
rename from rust-libs/duniter-gva/src/schema.rs
rename to rust-libs/modules/duniter-gva/src/schema.rs
index 9618f6ee7..c830bb85b 100644
--- a/rust-libs/duniter-gva/src/schema.rs
+++ b/rust-libs/modules/duniter-gva/src/schema.rs
@@ -21,7 +21,6 @@ pub(crate) type GraphQlSchema = async_graphql::Schema<
     crate::subscriptions::SubscriptionRoot,
 >;
 pub(crate) struct SchemaData {
-    pub(crate) dbs: DuniterDbs,
     pub(crate) dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<DuniterDbs>,
     pub(crate) server_meta_data: ServerMetaData,
     pub(crate) txs_mempool: TxsMempool,
diff --git a/rust-libs/duniter-gva/src/subscriptions.rs b/rust-libs/modules/duniter-gva/src/subscriptions.rs
similarity index 90%
rename from rust-libs/duniter-gva/src/subscriptions.rs
rename to rust-libs/modules/duniter-gva/src/subscriptions.rs
index 5be753736..7dd697b27 100644
--- a/rust-libs/duniter-gva/src/subscriptions.rs
+++ b/rust-libs/modules/duniter-gva/src/subscriptions.rs
@@ -28,11 +28,10 @@ impl SubscriptionRoot {
 
         let (s, r) = flume::unbounded();
 
-        data.dbs
-            .txs_mp_db
-            .txs()
-            .subscribe(s)
-            .expect("fail to access db");
+        data.dbs_pool
+            .execute(|dbs| dbs.txs_mp_db.txs().subscribe(s).expect("fail to access db"))
+            .await
+            .expect("dbs pool disconnected");
 
         r.into_stream().filter_map(|events| {
             let mut txs = Vec::new();
diff --git a/rust-libs/duniter-gva/src/warp_.rs b/rust-libs/modules/duniter-gva/src/warp_.rs
similarity index 98%
rename from rust-libs/duniter-gva/src/warp_.rs
rename to rust-libs/modules/duniter-gva/src/warp_.rs
index bc8aba0c7..5bf78f67a 100644
--- a/rust-libs/duniter-gva/src/warp_.rs
+++ b/rust-libs/modules/duniter-gva/src/warp_.rs
@@ -55,7 +55,7 @@ pub(crate) fn graphql(
     opts: async_graphql::http::MultipartOptions,
 ) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone {
     let opts = Arc::new(opts);
-    warp::path::path(conf.path.clone())
+    warp::path::path(conf.get_path())
         .and(warp::method())
         .and(warp::query::raw().or(warp::any().map(String::new)).unify())
         .and(warp::header::optional::<String>("content-type"))
@@ -100,7 +100,7 @@ pub(crate) fn graphql_ws(
     conf: &GvaConf,
     schema: GraphQlSchema,
 ) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone {
-    warp::path::path(conf.subscriptions_path.clone())
+    warp::path::path(conf.get_subscriptions_path())
         .and(warp::ws())
         .and(warp::any().map(move || schema.clone()))
         .map(|ws: warp::ws::Ws, schema: GraphQlSchema| {
diff --git a/server.ts b/server.ts
index ca8e26559..2387e4802 100644
--- a/server.ts
+++ b/server.ts
@@ -353,8 +353,13 @@ export class Server extends stream.Duplex implements HookableServer {
   }
 
   async initDAL(conf:ConfDTO|null = null, commandName: string|null = null) {
-    this.genGvaEndpoints(this.conf);
+    // Init DAL
     await this.dal.init(this.conf, commandName);
+    // Get rust endpoints
+    for (let endpoint of this.dal.getRustEndpoints()) {
+      logger.info("TMP: rustEndpoint: %s", endpoint);
+      this.addEndpointsDefinitions(async () => endpoint);
+    }
     // Maintenance
     let head_1 = await this.dal.bindexDAL.head(1);
     if (head_1) {
@@ -383,36 +388,6 @@ export class Server extends stream.Duplex implements HookableServer {
     await this.BlockchainService.forkResolution()
   }
 
-  genGvaEndpoint(conf: ConfDTO): string {
-    return ""
-  }
-  
-  genGvaSubscriptionsEndpoint(conf: ConfDTO): string {
-    return ""
-  }
-
-  genGvaEndpoints(conf:ConfDTO) {
-    if (conf.gva) {
-      let gva = conf.gva;
-      this.addEndpointsDefinitions(async () => format(
-        "GVA %s%s %i %s",
-        (gva.remotePort === 443 || gva.remoteTls === true) ? 'S ':'',
-        gva.remoteHost || gva.host || "localhost",
-        gva.remotePort || gva.port || 80,
-        gva.remotePath || gva.path || ""
-      ));
-      if (gva.remoteSubscriptionsPath || gva.subscriptionsPath) {
-        this.addEndpointsDefinitions(async () => format(
-          "GVASUB %s%s %i %s",
-          (gva.remotePort === 443 || gva.remoteTls === true) ? 'S ':'',
-          gva.remoteHost || gva.host || "localhost",
-          gva.remotePort || gva.port || 80,
-          gva.remoteSubscriptionsPath || gva.subscriptionsPath || ""
-        ));
-      }
-    }
-  }
-
   recomputeSelfPeer(): Promise<DBPeer | null> {
     return this.PeeringService.generateSelfPeer(this.conf)
   }
-- 
GitLab