From 2f73e4bce0c0273cced981e21b97c93ad25d0ac2 Mon Sep 17 00:00:00 2001
From: librelois <c@elo.tf>
Date: Thu, 3 Jun 2021 20:43:22 +0200
Subject: [PATCH] feat(gql): add query wallets

---
 Cargo.lock                     |   1 +
 dbs-reader/Cargo.toml          |   1 +
 dbs-reader/src/cursors.rs      |  83 +++++++++
 dbs-reader/src/lib.rs          |  70 +++++++-
 dbs-reader/src/pagination.rs   |  13 ++
 dbs-reader/src/wallets.rs      | 319 +++++++++++++++++++++++++++++++++
 gql/src/entities.rs            |  11 +-
 gql/src/entities/idty_gva.rs   |   2 +-
 gql/src/entities/wallet_gva.rs |  41 +++++
 gql/src/lib.rs                 |   1 +
 gql/src/queries.rs             |   2 +
 gql/src/queries/wallets.rs     | 172 ++++++++++++++++++
 12 files changed, 705 insertions(+), 11 deletions(-)
 create mode 100644 dbs-reader/src/cursors.rs
 create mode 100644 dbs-reader/src/wallets.rs
 create mode 100644 gql/src/entities/wallet_gva.rs
 create mode 100644 gql/src/queries/wallets.rs

diff --git a/Cargo.lock b/Cargo.lock
index c9d829c..fe38fd1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -984,6 +984,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "arrayvec 0.7.0",
+ "bincode",
  "duniter-core",
  "duniter-gva-db",
  "flate2",
diff --git a/dbs-reader/Cargo.toml b/dbs-reader/Cargo.toml
index 3063442..b25a488 100644
--- a/dbs-reader/Cargo.toml
+++ b/dbs-reader/Cargo.toml
@@ -17,6 +17,7 @@ mock = ["mockall"]
 [dependencies]
 anyhow = "1.0.34"
 arrayvec = { version = "0.7", features = ["serde"] }
+bincode = "1.3"
 duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core" }
 duniter-gva-db = { path = "../db" }
 flate2 = { version = "1.0", features = ["zlib-ng-compat"], default-features = false }
diff --git a/dbs-reader/src/cursors.rs b/dbs-reader/src/cursors.rs
new file mode 100644
index 0000000..b23b938
--- /dev/null
+++ b/dbs-reader/src/cursors.rs
@@ -0,0 +1,83 @@
+//  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::*;
+use duniter_core::crypto::keys::ed25519::PublicKey;
+use duniter_core::crypto::keys::PublicKey as _;
+use duniter_core::dbs::WalletConditionsV2;
+
+#[derive(Clone, Copy, Debug)]
+pub struct WrongCursor;
+impl std::fmt::Display for WrongCursor {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "wrong cursor")
+    }
+}
+impl std::error::Error for WrongCursor {}
+
+pub trait Cursor:
+    'static + Clone + std::fmt::Debug + std::fmt::Display + Default + FromStr + Ord
+{
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub struct PubKeyCursor(pub PublicKey);
+
+impl PubKeyCursor {
+    pub fn from_ref(pk: &PublicKey) -> &Self {
+        #[allow(trivial_casts)]
+        unsafe {
+            &*(pk as *const PublicKey as *const PubKeyCursor)
+        }
+    }
+}
+
+impl Cursor for PubKeyCursor {}
+
+impl std::fmt::Display for PubKeyCursor {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0.to_string())
+    }
+}
+
+impl FromStr for PubKeyCursor {
+    type Err = WrongCursor;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Ok(pk) = PublicKey::from_base58(s) {
+            Ok(PubKeyCursor(pk))
+        } else {
+            Err(WrongCursor)
+        }
+    }
+}
+
+impl From<PubKeyCursor> for WalletConditionsV2 {
+    fn from(val: PubKeyCursor) -> Self {
+        WalletConditionsV2(WalletScriptV10::single_sig(val.0))
+    }
+}
+
+impl Ord for PubKeyCursor {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.0.as_ref().cmp(other.0.as_ref())
+    }
+}
+
+impl PartialOrd for PubKeyCursor {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        self.0.as_ref().partial_cmp(other.0.as_ref())
+    }
+}
diff --git a/dbs-reader/src/lib.rs b/dbs-reader/src/lib.rs
index 98fba11..0d45be7 100644
--- a/dbs-reader/src/lib.rs
+++ b/dbs-reader/src/lib.rs
@@ -25,6 +25,7 @@
 pub mod block;
 pub mod blocks_chunks;
 pub mod current_frame;
+pub mod cursors;
 pub mod find_inputs;
 pub mod idty;
 pub mod network;
@@ -32,7 +33,9 @@ pub mod pagination;
 pub mod txs_history;
 pub mod uds_of_pubkey;
 pub mod utxos;
+pub mod wallets;
 
+pub use crate::cursors::{Cursor, PubKeyCursor, WrongCursor};
 pub use crate::pagination::{PageInfo, PagedData};
 pub use duniter_core::bda_types::MAX_FIRST_UTXOS;
 
@@ -69,15 +72,6 @@ use std::{
     str::FromStr,
 };
 
-#[derive(Clone, Copy, Debug)]
-pub struct WrongCursor;
-impl std::fmt::Display for WrongCursor {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "wrong cursor")
-    }
-}
-impl std::error::Error for WrongCursor {}
-
 #[cfg_attr(feature = "mock", mockall::automock)]
 pub trait DbsReader {
     fn all_uds_of_pubkey(
@@ -181,6 +175,29 @@ pub trait DbsReader {
         bn_to_exclude_opt: Option<std::collections::BTreeSet<BlockNumber>>,
         amount_target_opt: Option<SourceAmount>,
     ) -> KvResult<PagedData<uds_of_pubkey::UdsWithSum>>;
+    fn wallets(
+        &self,
+        exclude_single_sig: bool,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<wallets::WalletCursor>,
+    ) -> KvResult<PagedData<Vec<wallets::ScriptWithBalance>>>;
+    fn wallets_single_sig(
+        &self,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<PubKeyCursor>,
+    ) -> KvResult<PagedData<Vec<wallets::PublicKeyWithBalance>>>;
+    fn wallets_single_sig_with_idty_opt(
+        &self,
+        bc_db: &BcV2DbRo<FileBackend>,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<PubKeyCursor>,
+    ) -> KvResult<PagedData<Vec<wallets::WalletSingleSigWithIdtyOpt>>>;
+    fn wallets_with_idty_opt(
+        &self,
+        bc_db: &BcV2DbRo<FileBackend>,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<wallets::WalletCursor>,
+    ) -> KvResult<PagedData<Vec<wallets::WalletWithIdtyOpt>>>;
 }
 
 #[derive(Clone, Copy, Debug)]
@@ -359,6 +376,41 @@ impl DbsReader for DbsReaderImpl {
             amount_target_opt,
         )
     }
+
+    fn wallets(
+        &self,
+        exclude_single_sig: bool,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<wallets::WalletCursor>,
+    ) -> KvResult<PagedData<Vec<wallets::ScriptWithBalance>>> {
+        self.wallets_(exclude_single_sig, min_balance_opt, page_info)
+    }
+
+    fn wallets_single_sig(
+        &self,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<PubKeyCursor>,
+    ) -> KvResult<PagedData<Vec<wallets::PublicKeyWithBalance>>> {
+        self.wallets_single_sig_(min_balance_opt, page_info)
+    }
+
+    fn wallets_single_sig_with_idty_opt(
+        &self,
+        bc_db: &BcV2DbRo<FileBackend>,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<PubKeyCursor>,
+    ) -> KvResult<PagedData<Vec<wallets::WalletSingleSigWithIdtyOpt>>> {
+        self.wallets_single_sig_with_idty_opt_(bc_db, min_balance_opt, page_info)
+    }
+
+    fn wallets_with_idty_opt(
+        &self,
+        bc_db: &BcV2DbRo<FileBackend>,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<wallets::WalletCursor>,
+    ) -> KvResult<PagedData<Vec<wallets::WalletWithIdtyOpt>>> {
+        self.wallets_with_idty_opt_(bc_db, min_balance_opt, page_info)
+    }
 }
 
 #[cfg(test)]
diff --git a/dbs-reader/src/pagination.rs b/dbs-reader/src/pagination.rs
index 48d80bb..8cd2931 100644
--- a/dbs-reader/src/pagination.rs
+++ b/dbs-reader/src/pagination.rs
@@ -30,6 +30,19 @@ impl<D: std::fmt::Debug + Default> PagedData<D> {
         }
     }
 }
+impl<D: std::fmt::Debug> PagedData<D> {
+    pub fn map<F, T>(self, f: F) -> PagedData<T>
+    where
+        T: std::fmt::Debug,
+        F: FnOnce(D) -> T,
+    {
+        PagedData {
+            data: f(self.data),
+            has_previous_page: self.has_previous_page,
+            has_next_page: self.has_next_page,
+        }
+    }
+}
 
 #[derive(Debug)]
 pub struct PageInfo<T> {
diff --git a/dbs-reader/src/wallets.rs b/dbs-reader/src/wallets.rs
new file mode 100644
index 0000000..a6dca72
--- /dev/null
+++ b/dbs-reader/src/wallets.rs
@@ -0,0 +1,319 @@
+//  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::*;
+use duniter_core::crypto::keys::ed25519::PublicKey;
+use duniter_core::crypto::keys::PublicKey as _;
+use duniter_core::dbs::{bincode_db, IdtyDbV2, WalletConditionsV2};
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct WalletCursor(WalletScriptV10);
+impl WalletCursor {
+    pub fn from_ref(script: &WalletScriptV10) -> &Self {
+        #[allow(trivial_casts)]
+        unsafe {
+            &*(script as *const WalletScriptV10 as *const WalletCursor)
+        }
+    }
+}
+
+impl Cursor for WalletCursor {}
+
+impl Default for WalletCursor {
+    fn default() -> Self {
+        WalletCursor(WalletScriptV10::single_sig(PublicKey::default()))
+    }
+}
+
+impl std::fmt::Display for WalletCursor {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0.to_string())
+    }
+}
+
+impl FromStr for WalletCursor {
+    type Err = WrongCursor;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Ok(pubkey) = PublicKey::from_base58(s) {
+            Ok(WalletCursor(WalletScriptV10::single_sig(pubkey)))
+        } else if let Ok(wallet_script) = duniter_core::documents_parser::wallet_script_from_str(s)
+        {
+            Ok(WalletCursor(wallet_script))
+        } else {
+            Err(WrongCursor)
+        }
+    }
+}
+
+impl Ord for WalletCursor {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        use bincode::config::Options as _;
+        let self_bin = bincode_db()
+            .serialize(&self.0)
+            .unwrap_or_else(|_| unreachable!());
+        let other_bin = bincode_db()
+            .serialize(&other.0)
+            .unwrap_or_else(|_| unreachable!());
+        self_bin.cmp(&other_bin)
+    }
+}
+
+impl From<WalletCursor> for WalletConditionsV2 {
+    fn from(val: WalletCursor) -> Self {
+        WalletConditionsV2(val.0)
+    }
+}
+
+impl PartialOrd for WalletCursor {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        use bincode::config::Options as _;
+        let self_bin = bincode_db()
+            .serialize(&self.0)
+            .unwrap_or_else(|_| unreachable!());
+        let other_bin = bincode_db()
+            .serialize(&other.0)
+            .unwrap_or_else(|_| unreachable!());
+        self_bin.partial_cmp(&other_bin)
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct PublicKeyWithBalance(pub PublicKey, pub SourceAmount);
+impl AsRef<PubKeyCursor> for PublicKeyWithBalance {
+    fn as_ref(&self) -> &PubKeyCursor {
+        PubKeyCursor::from_ref(&self.0)
+    }
+}
+
+#[derive(Debug)]
+pub struct ScriptWithBalance(pub WalletScriptV10, pub SourceAmount);
+impl AsRef<WalletCursor> for ScriptWithBalance {
+    fn as_ref(&self) -> &WalletCursor {
+        WalletCursor::from_ref(&self.0)
+    }
+}
+
+#[derive(Debug)]
+pub struct WalletSingleSigWithIdtyOpt(pub PublicKeyWithBalance, pub Option<IdtyDbV2>);
+
+#[derive(Debug)]
+pub struct WalletWithIdtyOpt(pub ScriptWithBalance, pub Option<IdtyDbV2>);
+
+impl DbsReaderImpl {
+    pub(super) fn wallets_(
+        &self,
+        exclude_single_sig: bool,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<WalletCursor>,
+    ) -> KvResult<PagedData<Vec<ScriptWithBalance>>> {
+        if let Some(min_balance) = min_balance_opt {
+            if exclude_single_sig {
+                self.wallets_inner(
+                    |(k, v)| {
+                        if !k.0.is_single_sig() && v.0 >= min_balance {
+                            Some(ScriptWithBalance(k.0, v.0))
+                        } else {
+                            None
+                        }
+                    },
+                    page_info,
+                )
+            } else {
+                self.wallets_inner(
+                    |(k, v)| {
+                        if v.0 >= min_balance {
+                            Some(ScriptWithBalance(k.0, v.0))
+                        } else {
+                            None
+                        }
+                    },
+                    page_info,
+                )
+            }
+        } else if exclude_single_sig {
+            self.wallets_inner(
+                |(k, v)| {
+                    if !k.0.is_single_sig() {
+                        Some(ScriptWithBalance(k.0, v.0))
+                    } else {
+                        None
+                    }
+                },
+                page_info,
+            )
+        } else {
+            self.wallets_inner(|(k, v)| Some(ScriptWithBalance(k.0, v.0)), page_info)
+        }
+    }
+
+    pub(super) fn wallets_with_idty_opt_(
+        &self,
+        bc_db: &BcV2DbRo<FileBackend>,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<WalletCursor>,
+    ) -> KvResult<PagedData<Vec<WalletWithIdtyOpt>>> {
+        let paged_data = self.wallets_(false, min_balance_opt, page_info)?;
+
+        let mut data = Vec::with_capacity(paged_data.data.len());
+        for script_with_balance in paged_data.data {
+            let idty_opt = if let Some(pubkey) = script_with_balance.0.as_single_sig() {
+                bc_db.identities().get(&PubKeyKeyV2(pubkey))?
+            } else {
+                None
+            };
+
+            data.push(WalletWithIdtyOpt(script_with_balance, idty_opt));
+        }
+
+        Ok(PagedData {
+            data,
+            has_next_page: paged_data.has_next_page,
+            has_previous_page: paged_data.has_previous_page,
+        })
+    }
+
+    pub(super) fn wallets_single_sig_(
+        &self,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<PubKeyCursor>,
+    ) -> KvResult<PagedData<Vec<PublicKeyWithBalance>>> {
+        if let Some(min_balance) = min_balance_opt {
+            self.wallets_inner(
+                |(k, v)| {
+                    if v.0 >= min_balance {
+                        k.0.as_single_sig().map(|pk| PublicKeyWithBalance(pk, v.0))
+                    } else {
+                        None
+                    }
+                },
+                page_info,
+            )
+        } else {
+            self.wallets_inner(
+                |(k, v)| k.0.as_single_sig().map(|pk| PublicKeyWithBalance(pk, v.0)),
+                page_info,
+            )
+        }
+    }
+
+    pub(super) fn wallets_single_sig_with_idty_opt_(
+        &self,
+        bc_db: &BcV2DbRo<FileBackend>,
+        min_balance_opt: Option<SourceAmount>,
+        page_info: PageInfo<PubKeyCursor>,
+    ) -> KvResult<PagedData<Vec<WalletSingleSigWithIdtyOpt>>> {
+        let paged_data = self.wallets_single_sig_(min_balance_opt, page_info)?;
+
+        let mut data = Vec::with_capacity(paged_data.data.len());
+        for pk_with_balance in paged_data.data {
+            let idty_opt = bc_db.identities().get(&PubKeyKeyV2(pk_with_balance.0))?;
+            data.push(WalletSingleSigWithIdtyOpt(pk_with_balance, idty_opt));
+        }
+
+        Ok(PagedData {
+            data,
+            has_next_page: paged_data.has_next_page,
+            has_previous_page: paged_data.has_previous_page,
+        })
+    }
+
+    fn wallets_inner<C, E, F>(
+        &self,
+        filter_map: F,
+        page_info: PageInfo<C>,
+    ) -> KvResult<PagedData<Vec<E>>>
+    where
+        C: Cursor + Into<WalletConditionsV2>,
+        E: AsRef<C> + std::fmt::Debug + Send + Sync,
+        F: Copy + Fn((WalletConditionsV2, SourceAmountValV2)) -> Option<E>,
+    {
+        let first_cursor_opt = if page_info.not_all() {
+            self.0
+                .balances()
+                .iter(.., |it| it.filter_map_ok(filter_map).next_res())?
+                .map(|element| element.as_ref().to_owned())
+        } else {
+            None
+        };
+        let last_cursor_opt = if page_info.not_all() {
+            self.0
+                .balances()
+                .iter_rev(.., |it| it.filter_map_ok(filter_map).next_res())?
+                .map(|element| element.as_ref().to_owned())
+        } else {
+            None
+        };
+
+        let cursor_opt = page_info.pos.clone();
+        let data = if page_info.order {
+            let first_key = cursor_opt
+                .unwrap_or_else(|| first_cursor_opt.clone().unwrap_or_default())
+                .into();
+            self.0.balances().iter(first_key.., |it| {
+                if let Some(limit) = page_info.limit_opt {
+                    it.filter_map_ok(filter_map)
+                        .take(limit.get())
+                        .collect::<KvResult<Vec<_>>>()
+                } else {
+                    it.filter_map_ok(filter_map).collect::<KvResult<Vec<_>>>()
+                }
+            })?
+        } else {
+            let last_key = cursor_opt
+                .unwrap_or_else(|| last_cursor_opt.clone().unwrap_or_default())
+                .into();
+            self.0.balances().iter_rev(..=last_key, |it| {
+                if let Some(limit) = page_info.limit_opt {
+                    it.filter_map_ok(filter_map)
+                        .take(limit.get())
+                        .collect::<KvResult<Vec<_>>>()
+                } else {
+                    it.filter_map_ok(filter_map).collect::<KvResult<Vec<_>>>()
+                }
+            })?
+        };
+
+        let page_not_reversed = page_info.order;
+
+        Ok(PagedData {
+            has_next_page: if page_info.order {
+                has_next_page(
+                    data.iter()
+                        .map(|element| OwnedOrRef::Borrow(element.as_ref())),
+                    last_cursor_opt,
+                    page_info.clone(),
+                    page_not_reversed,
+                )
+            } else {
+                // Server can't efficiently determine hasNextPage in DESC order
+                false
+            },
+            has_previous_page: if page_info.order {
+                // Server can't efficiently determine hasPreviousPage in ASC order
+                false
+            } else {
+                has_previous_page(
+                    data.iter()
+                        .map(|element| OwnedOrRef::Borrow(element.as_ref())),
+                    first_cursor_opt,
+                    page_info,
+                    page_not_reversed,
+                )
+            },
+            data,
+        })
+    }
+}
diff --git a/gql/src/entities.rs b/gql/src/entities.rs
index a7f5102..b2c93db 100644
--- a/gql/src/entities.rs
+++ b/gql/src/entities.rs
@@ -19,6 +19,7 @@ pub mod network;
 pub mod tx_gva;
 pub mod ud_gva;
 pub mod utxos_gva;
+pub mod wallet_gva;
 
 use crate::*;
 
@@ -27,11 +28,19 @@ pub(crate) struct AggregateSum {
     pub(crate) aggregate: Sum,
 }
 
-#[derive(Default, async_graphql::SimpleObject)]
+#[derive(Clone, Copy, Debug, Default, async_graphql::SimpleObject)]
 pub(crate) struct AmountWithBase {
     pub(crate) amount: i32,
     pub(crate) base: i32,
 }
+impl From<SourceAmount> for AmountWithBase {
+    fn from(sa: SourceAmount) -> Self {
+        Self {
+            amount: sa.amount() as i32,
+            base: sa.base() as i32,
+        }
+    }
+}
 
 #[derive(async_graphql::SimpleObject)]
 pub(crate) struct EdgeTx {
diff --git a/gql/src/entities/idty_gva.rs b/gql/src/entities/idty_gva.rs
index ee77b0e..8729895 100644
--- a/gql/src/entities/idty_gva.rs
+++ b/gql/src/entities/idty_gva.rs
@@ -13,7 +13,7 @@
 // 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/>.
 
-#[derive(async_graphql::SimpleObject)]
+#[derive(Clone, Debug, async_graphql::SimpleObject)]
 pub(crate) struct Identity {
     pub is_member: bool,
     pub username: String,
diff --git a/gql/src/entities/wallet_gva.rs b/gql/src/entities/wallet_gva.rs
new file mode 100644
index 0000000..dfab890
--- /dev/null
+++ b/gql/src/entities/wallet_gva.rs
@@ -0,0 +1,41 @@
+//  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, Copy, async_graphql::Enum, Eq, PartialEq)]
+pub(crate) enum WalletTypeFilter {
+    /// All wallets
+    All,
+    /// Exclude wallets scripts with single SIG condition
+    OnlyComplex,
+    /// Only wallets scripts with single SIG condition
+    OnlySimple,
+}
+impl Default for WalletTypeFilter {
+    fn default() -> WalletTypeFilter {
+        WalletTypeFilter::OnlySimple
+    }
+}
+
+#[derive(Clone, Debug, async_graphql::SimpleObject)]
+pub(crate) struct Wallet {
+    /// Wallet script or public key
+    pub(crate) script: String,
+    /// Wallet balance
+    pub(crate) balance: AmountWithBase,
+    /// Optional identity attached to this wallet
+    pub(crate) idty: Option<Identity>,
+}
diff --git a/gql/src/lib.rs b/gql/src/lib.rs
index e801b81..1d91438 100644
--- a/gql/src/lib.rs
+++ b/gql/src/lib.rs
@@ -41,6 +41,7 @@ use crate::entities::{
     tx_gva::{PendingTxGva, WrittenTxGva},
     ud_gva::{CurrentUdGva, RevalUdGva, UdGva},
     utxos_gva::UtxosGva,
+    wallet_gva::{Wallet, WalletTypeFilter},
     AggregateSum, AmountWithBase, EdgeTx, RawTxOrChanges, Sum, TxDirection, TxsHistoryMempool,
     UtxoGva, UtxoTimedGva,
 };
diff --git a/gql/src/queries.rs b/gql/src/queries.rs
index ad733e3..1026447 100644
--- a/gql/src/queries.rs
+++ b/gql/src/queries.rs
@@ -24,6 +24,7 @@ pub mod network;
 pub mod txs_history;
 pub mod uds;
 pub mod utxos_of_script;
+pub mod wallets;
 
 use crate::*;
 
@@ -42,6 +43,7 @@ pub struct QueryRoot(
     queries::txs_history::TxsHistoryMempoolQuery,
     queries::uds::UdsQuery,
     queries::utxos_of_script::UtxosQuery,
+    queries::wallets::WalletsQuery,
 );
 
 #[derive(Default, async_graphql::SimpleObject)]
diff --git a/gql/src/queries/wallets.rs b/gql/src/queries/wallets.rs
new file mode 100644
index 0000000..699a085
--- /dev/null
+++ b/gql/src/queries/wallets.rs
@@ -0,0 +1,172 @@
+//  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::*;
+use async_graphql::connection::*;
+use duniter_gva_dbs_reader::{
+    wallets::{WalletSingleSigWithIdtyOpt, WalletWithIdtyOpt},
+    PagedData,
+};
+
+#[derive(Default)]
+pub(crate) struct WalletsQuery;
+#[async_graphql::Object]
+impl WalletsQuery {
+    /// Universal dividends issued by a public key
+    #[allow(clippy::clippy::too_many_arguments)]
+    async fn wallets(
+        &self,
+        ctx: &async_graphql::Context<'_>,
+        #[graphql(desc = "minimal balance")] min_balance: Option<i64>,
+        #[graphql(desc = "pagination", default)] pagination: Pagination,
+        #[graphql(desc = "Wallet type filter", default)] wallet_type_filter: WalletTypeFilter,
+    ) -> async_graphql::Result<Connection<String, Wallet, EmptyFields, EmptyFields>> {
+        let QueryContext { is_whitelisted } = ctx.data::<QueryContext>()?;
+
+        let data = ctx.data::<GvaSchemaData>()?;
+        let dbs_reader = data.dbs_reader();
+
+        let current_base =
+            if let Some(current_ud) = data.cm_accessor.get_current_meta(|cm| cm.current_ud).await {
+                current_ud.base()
+            } else {
+                0
+            };
+        let min_balance_opt = min_balance.map(|amount| SourceAmount::new(amount, current_base));
+
+        let PagedData {
+            data,
+            has_next_page,
+            has_previous_page,
+        }: PagedData<Vec<Wallet>> = match wallet_type_filter {
+            WalletTypeFilter::OnlyComplex => {
+                let pagination = Pagination::convert_to_page_info(pagination, *is_whitelisted)?;
+                data.dbs_pool
+                    .execute(move |_| dbs_reader.wallets(true, min_balance_opt, pagination))
+                    .await??
+                    .map(|data| {
+                        data.into_iter()
+                            .map(|script_with_sa| Wallet {
+                                script: script_with_sa.0.to_string(),
+                                balance: AmountWithBase::from(script_with_sa.1),
+                                idty: None,
+                            })
+                            .collect()
+                    })
+            }
+            WalletTypeFilter::OnlySimple => {
+                let pagination = Pagination::convert_to_page_info(pagination, *is_whitelisted)?;
+                if ctx
+                    .look_ahead()
+                    .field("edges")
+                    .field("node")
+                    .field("idty")
+                    .exists()
+                {
+                    data.dbs_pool
+                        .execute(move |shared_dbs| {
+                            dbs_reader.wallets_single_sig_with_idty_opt(
+                                &shared_dbs.bc_db_ro,
+                                min_balance_opt,
+                                pagination,
+                            )
+                        })
+                        .await??
+                        .map(|data| {
+                            data.into_iter()
+                                .map(|WalletSingleSigWithIdtyOpt(pk_with_sa, idty_opt)| Wallet {
+                                    script: pk_with_sa.0.to_string(),
+                                    balance: AmountWithBase::from(pk_with_sa.1),
+                                    idty: idty_opt.map(|idty_db| Identity {
+                                        is_member: idty_db.is_member,
+                                        username: idty_db.username,
+                                    }),
+                                })
+                                .collect()
+                        })
+                } else {
+                    data.dbs_pool
+                        .execute(move |_| {
+                            dbs_reader.wallets_single_sig(min_balance_opt, pagination)
+                        })
+                        .await??
+                        .map(|data| {
+                            data.into_iter()
+                                .map(|pk_with_sa| Wallet {
+                                    script: pk_with_sa.0.to_string(),
+                                    balance: AmountWithBase::from(pk_with_sa.1),
+                                    idty: None,
+                                })
+                                .collect()
+                        })
+                }
+            }
+            WalletTypeFilter::All => {
+                let pagination = Pagination::convert_to_page_info(pagination, *is_whitelisted)?;
+                if ctx
+                    .look_ahead()
+                    .field("edges")
+                    .field("node")
+                    .field("idty")
+                    .exists()
+                {
+                    data.dbs_pool
+                        .execute(move |shared_dbs| {
+                            dbs_reader.wallets_with_idty_opt(
+                                &shared_dbs.bc_db_ro,
+                                min_balance_opt,
+                                pagination,
+                            )
+                        })
+                        .await??
+                        .map(|data| {
+                            data.into_iter()
+                                .map(|WalletWithIdtyOpt(script_with_sa, idty_opt)| Wallet {
+                                    script: script_with_sa.0.to_string(),
+                                    balance: AmountWithBase::from(script_with_sa.1),
+                                    idty: idty_opt.map(|idty_db| Identity {
+                                        is_member: idty_db.is_member,
+                                        username: idty_db.username,
+                                    }),
+                                })
+                                .collect()
+                        })
+                } else {
+                    data.dbs_pool
+                        .execute(move |_| dbs_reader.wallets(false, min_balance_opt, pagination))
+                        .await??
+                        .map(|data| {
+                            data.into_iter()
+                                .map(|script_with_sa| Wallet {
+                                    script: script_with_sa.0.to_string(),
+                                    balance: AmountWithBase::from(script_with_sa.1),
+                                    idty: None,
+                                })
+                                .collect()
+                        })
+                }
+            }
+        };
+
+        let mut conn = Connection::new(has_previous_page, has_next_page);
+
+        conn.append(
+            data.into_iter()
+                .map(|wallet| Edge::new(wallet.script.clone(), wallet)),
+        );
+
+        Ok(conn)
+    }
+}
-- 
GitLab