From aa0fc5c7c552758ce01c798325aaba318bce2d6f Mon Sep 17 00:00:00 2001
From: vjrj <vjrj@comunes.org>
Date: Sat, 18 Mar 2023 18:46:22 +0100
Subject: [PATCH] More work with node management

---
 assets/.env.development                       |  2 +-
 assets/env.production.txt                     |  2 +-
 assets/translations/en.json                   |  8 +--
 assets/translations/es.json                   |  7 ++-
 assets/translations/fr.json                   |  5 +-
 lib/g1/api.dart                               | 26 +++++----
 lib/main.dart                                 | 22 ++++----
 lib/ui/screens/fifth_screen.dart              | 53 ++++++++++++-------
 .../pay_contact_search_dialog.dart            |  4 +-
 9 files changed, 77 insertions(+), 52 deletions(-)

diff --git a/assets/.env.development b/assets/.env.development
index 59fa80aa..3f67d03d 100644
--- a/assets/.env.development
+++ b/assets/.env.development
@@ -11,4 +11,4 @@ CARD_COLOR_TEXT=Äž1 Wallet Dev
 # The duniter nodes are only used at boot time, later it tries to calculate periodically the nodes
 # that are available with the less latency
 DUNITER_NODES=https://g1.duniter.fr https://g1.le-sou.org https://g1.cgeek.fr https://g1.monnaielibreoccitanie.org https://g1.duniter.fr https://g1.le-sou.org https://g1.cgeek.fr
-CESIUM_PLUS_NODES=https://g1.data.le-sou.org https://g1.data.e-is.pro https://g1.data.presler.fr https://g1.data.mithril.re g1.data.adn.life
+CESIUM_PLUS_NODES=https://g1.data.le-sou.org https://g1.data.e-is.pro https://g1.data.presler.fr https://g1.data.mithril.re https://g1.data.adn.life
diff --git a/assets/env.production.txt b/assets/env.production.txt
index 744510e6..eff26550 100644
--- a/assets/env.production.txt
+++ b/assets/env.production.txt
@@ -10,5 +10,5 @@ CARD_COLOR_TEXT=Äž1 Wallet Cop
 # The duniter nodes are only used at boot time, later it tries to calculate periodically the nodes
 # that are available with the less latency
 DUNITER_NODES=https://g1.duniter.fr https://g1.le-sou.org https://g1.cgeek.fr https://g1.monnaielibreoccitanie.org https://g1.duniter.fr https://g1.le-sou.org https://g1.cgeek.fr
-CESIUM_PLUS_NODES=https://g1.data.le-sou.org https://g1.data.e-is.pro https://g1.data.presler.fr https://g1.data.mithril.re g1.data.adn.life
+CESIUM_PLUS_NODES=https://g1.data.le-sou.org https://g1.data.e-is.pro https://g1.data.presler.fr https://g1.data.mithril.re https://g1.data.adn.life
 
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 0b9b64f0..dff1789f 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -40,7 +40,6 @@
   "card_validity": "Validity",
   "card_validity_tooltip": "Please note that this wallet is only accessible while using this specific browser and device. If you delete or reset the browser, you will lose access to this wallet and the funds stored in it.",
   "demo_desc": "Please refrain from using this with real transactions for now.",
-  "connected_to": "We are connected to node:",
   "export_key": "Export your wallet",
   "import_key": "Import your wallet",
   "copy_your_key": "Copy your public key",
@@ -57,9 +56,12 @@
   "data_load_error": "Error loading data",
   "add_contact": "Add contact",
   "contact_added": "Contact added",
-  "current_nodes_length": "(of {nodes})",
   "no_transactions": "You don't have any transaction yet",
   "qr-scanner-title": "Scan the QR of someone",
   "copy_contact_key": "Copy",
-  "nothing_found": "Nothing found"
+  "nothing_found": "Nothing found",
+  "using_nodes": "Using {nodes} nodes of {type}",
+  "using_nodes_first": "Faster: {node}",
+  "long_press_to_refresh": "Long press to refresh",
+  "technical_info_title": "Technical info"
 }
diff --git a/assets/translations/es.json b/assets/translations/es.json
index c0e01c43..a49a0ee5 100644
--- a/assets/translations/es.json
+++ b/assets/translations/es.json
@@ -40,7 +40,6 @@
   "card_validity": "Validez",
   "card_validity_tooltip": "Tenga en cuenta que este monedero solo es accesible mientras utiliza este navegador y este dispositivo específico. Si borra o restablece el navegador, perderá el acceso a este monedero y los fondos almacenados en el.",
   "demo_desc": "Por favor, no utilice esto aún para transacciones reales.",
-  "connected_to": "Estamos conectados al nodo:",
   "export_key": "Exporta tu monedero",
   "import_key": "Importa tu monedero",
   "copy_your_key": "Copia tu clave pública",
@@ -60,5 +59,9 @@
   "no_transactions": "No tiene todavía ninguna transacción",
   "qr-scanner-title": "Escanea el QR de alguien",
   "copy_contact_key": "Copiar",
-  "nothing_found": "No se ha encontrado nada"
+  "nothing_found": "No se ha encontrado nada",
+  "using_nodes": "Usando {nodes} nodos de {type}",
+  "using_nodes_first": "El más rápido: {node}",
+  "long_press_to_refresh": "Mantén presionado para refrescar",
+  "technical_info_title": "Info técnica"
 }
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
index 0780bd5b..ee5dd4cf 100644
--- a/assets/translations/fr.json
+++ b/assets/translations/fr.json
@@ -8,7 +8,7 @@
   "bottom_nav_trd": "Contacts",
   "bottom_nav_frd": "Solde",
   "bottom_nav_fifth": "Info",
-  "send_g1: "Envoyer des Äž1",
+  "send_g1: "Envoyer" des Äž1",
   "g1_amount": "Montant à envoyer",
   "g1_amount_hint": "Montant à envoyer en Ğ1",
   "g1_form_pay_send": "Envoyer",
@@ -40,7 +40,6 @@
   "card_validity": "Validité",
   "card_validity_tooltip": "Veuillez noter que ce portefeuille n'est accessible que lors de l'utilisation de ce navigateur et de cet appareil spécifiques. Si vous supprimez ou réinitialisez le navigateur, vous perdrez l'accès à ce portefeuille et aux fonds qu'il contient.",
   "demo_desc": "Veuillez vous abstenir d'utiliser ceci avec de vraies transactions pour le moment.",
-  "connected_to": "Nous sommes connectés au nœud:",
   "export_key": "Exporter votre portefeuille",
   "import_key": "Importer votre portefeuille",
   "copy_your_key": "Copier votre clé publique",
@@ -53,5 +52,5 @@
   "delete_contact": "Supprimer le contact",
   "search_contacts": "Rechercher des contacts",
   "no_contacts": "Vous n'avez pas encore de contacts",
-   "data_load_error": "Erreur de chargement des données"
+  "data_load_error": "Erreur de chargement des données"
 }
diff --git a/lib/g1/api.dart b/lib/g1/api.dart
index c980c348..78d0698b 100644
--- a/lib/g1/api.dart
+++ b/lib/g1/api.dart
@@ -159,6 +159,7 @@ Future<List<Node>> _fetchDuniterNodesFromPeers() async {
   final List<Node> lNodes = <Node>[];
   // To compare with something...
   String fastestNode = 'https://g1.duniter.org';
+  const NodeType type = NodeType.duniter;
   late Duration fastestLatency = const Duration(minutes: 1);
   try {
     final Response response = await getPeers();
@@ -186,7 +187,7 @@ Future<List<Node>> _fetchDuniterNodesFromPeers() async {
               final String? endpoint = parseHost(endpointUnParsed);
               if (endpoint != null) {
                 try {
-                  final Duration latency = await _pingNode(endpoint);
+                  final Duration latency = await _pingNode(endpoint, type);
                   logger(
                       'Evaluating node: $endpoint, latency ${latency.inMicroseconds}');
                   final Node node =
@@ -197,11 +198,11 @@ Future<List<Node>> _fetchDuniterNodesFromPeers() async {
                     if (!kReleaseMode) {
                       logger('Node bloc: Current faster node $fastestNode');
                     }
-                    NodeManager().insertNode(NodeType.duniter, node);
+                    NodeManager().insertNode(type, node);
                     lNodes.insert(0, node);
                   } else {
                     // Not the faster
-                    NodeManager().addNode(NodeType.duniter, node);
+                    NodeManager().addNode(type, node);
                     lNodes.add(node);
                   }
                 } catch (e) {
@@ -211,7 +212,7 @@ Future<List<Node>> _fetchDuniterNodesFromPeers() async {
             }
           }
           if (lNodes.length >= NodeManager.maxNodes) {
-            logger('We have enought nodes for now');
+            logger('We have enough nodes for now');
             break;
           }
         }
@@ -219,8 +220,9 @@ Future<List<Node>> _fetchDuniterNodesFromPeers() async {
     }
     logger(
         'Fetched ${lNodes.length} duniter nodes ordered by latency (first: ${lNodes.first.url})');
-  } catch (e) {
+  } catch (e, stacktrace) {
     logger('General error in fetch duniter nodes: $e');
+    logger(stacktrace);
     // rethrow;
   }
   lNodes.sort((Node a, Node b) => a.latency.compareTo(b.latency));
@@ -234,13 +236,13 @@ Future<List<Node>> _fetchCesiumPlusNodes() async {
   late Duration fastestLatency = const Duration(minutes: 1);
   try {
     const NodeType type = NodeType.cesiumPlus;
-    final List<Node> currentNodes = NodeManager().nodeList(type);
+    final List<Node> currentNodes = <Node>[...NodeManager().nodeList(type)];
     currentNodes.shuffle();
     for (final Node node in currentNodes) {
       final String endpoint = node.url;
 
       try {
-        final Duration latency = await _pingNode(endpoint);
+        final Duration latency = await _pingNode(endpoint, type);
         logger('Evaluating node: $endpoint, latency ${latency.inMicroseconds}');
         final Node node = Node(url: endpoint, latency: latency.inMicroseconds);
         if (fastestNode == null || latency < fastestLatency) {
@@ -263,20 +265,22 @@ Future<List<Node>> _fetchCesiumPlusNodes() async {
 
     logger(
         'Fetched ${lNodes.length} cesium plus nodes ordered by latency (first: ${lNodes.first.url})');
-  } catch (e) {
+  } catch (e, stacktrace) {
     logger('General error in fetch cplus nodes: $e');
-    // rethrow;
+    logger(stacktrace);
   }
   lNodes.sort((Node a, Node b) => a.latency.compareTo(b.latency));
   logger('First node in list ${lNodes.first.url}');
   return lNodes;
 }
 
-Future<Duration> _pingNode(String node) async {
+Future<Duration> _pingNode(String node, NodeType type) async {
   try {
     final Stopwatch stopwatch = Stopwatch()..start();
     await http
-        .get(Uri.parse('$node/network/peers/self/ping'))
+        .get(Uri.parse(type == NodeType.duniter
+            ? '$node/network/peers/self/ping'
+            : '$node/node/summary'))
         // Decrease http timeout during ping
         .timeout(const Duration(seconds: 10));
     stopwatch.stop();
diff --git a/lib/main.dart b/lib/main.dart
index ceda0f11..04da424e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -205,28 +205,30 @@ class GinkgoApp extends StatefulWidget {
 }
 
 class _GinkgoAppState extends State<GinkgoApp> {
-  Future<void> _loadNodes(NodeListCubit cubit) async {
-    // Load nodes from /network/peers
-    NodeManager().loadFromCubit(cubit);
+  Future<void> _loadNodes() async {
+    _printNodeStatus();
+    await fetchDuniterNodes();
+    await fetchCesiumPlusNodes();
+    _printNodeStatus(prefix: 'Continuing');
+  }
+
+  void _printNodeStatus({String prefix = 'Starting'}) {
     final int nDuniterNodes = NodeManager().nodeList(NodeType.duniter).length;
     final int nCesiumPlusNodes =
         NodeManager().nodeList(NodeType.cesiumPlus).length;
     logger(
-        'Starting with $nDuniterNodes duniter nodes and $nCesiumPlusNodes c+ nodes');
-    await fetchDuniterNodes();
-    await fetchCesiumPlusNodes();
-    logger(
-        'Continue with $nDuniterNodes duniter nodes and $nCesiumPlusNodes c+ nodes');
+        '$prefix with $nDuniterNodes duniter nodes and $nCesiumPlusNodes c+ nodes');
   }
 
   @override
   Widget build(BuildContext context) {
     return BlocBuilder<NodeListCubit, NodeListState>(
         builder: (BuildContext nodeContext, NodeListState state) {
+      NodeManager().loadFromCubit(nodeContext.read<NodeListCubit>());
       Once.runHourly('load_nodes',
-          callback: () => _loadNodes(nodeContext.read<NodeListCubit>()),
+          callback: () => _loadNodes(),
           fallback: () {
-            logger('Finished once load nodes');
+            _printNodeStatus(prefix: 'After once hourly having');
           });
 
       Once.runCustom('clear_errors', callback: () {
diff --git a/lib/ui/screens/fifth_screen.dart b/lib/ui/screens/fifth_screen.dart
index c1c2ecf0..ab046a58 100644
--- a/lib/ui/screens/fifth_screen.dart
+++ b/lib/ui/screens/fifth_screen.dart
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import '../../data/models/node.dart';
 import '../../data/models/node_list_cubit.dart';
 import '../../data/models/node_list_state.dart';
+import '../../data/models/node_manager.dart';
 import '../../g1/api.dart';
 import '../../g1/export_import.dart';
 import '../../main.dart';
@@ -23,7 +24,6 @@ class FifthScreen extends StatelessWidget {
   Widget build(BuildContext context) {
     return BlocBuilder<NodeListCubit, NodeListState>(
         builder: (BuildContext nodeContext, NodeListState state) {
-      final List<Node> duniterNodes = state.duniterNodes;
       return Material(
         color: Theme.of(context).colorScheme.background,
         child: ListView(
@@ -31,24 +31,6 @@ class FifthScreen extends StatelessWidget {
             physics: const BouncingScrollPhysics(),
             children: <Widget>[
               const Header(text: 'bottom_nav_fifth'),
-              GestureDetector(
-                  onLongPress: () {
-                    logger('On long press');
-                    fetchDuniterNodes(force: true);
-                    fetchCesiumPlusNodes(force: true);
-                  },
-                  child: InfoCard(
-                      title: 'connected_to',
-                      subtitle: duniterNodes.first.url.replaceFirst(':443', ''),
-                      trailing: tr('current_nodes_length',
-                          namedArgs: <String, String>{
-                            'nodes': duniterNodes.length.toString()
-                          }),
-                      icon: Icons.hub)),
-              LinkCard(
-                  title: 'code_card_title',
-                  icon: Icons.code_rounded,
-                  url: Uri.parse('https://git.duniter.org/vjrj/ginkgo')),
               const TextDivider(text: 'key_tools_title'),
               GridView.count(
                   physics: const NeverScrollableScrollPhysics(),
@@ -82,9 +64,42 @@ class FifthScreen extends StatelessWidget {
                       onTap: () => copyPublicKeyToClipboard(context),
                     )
                   ]),
+              const TextDivider(text: 'technical_info_title'),
+              _buildNodeInfo(NodeType.duniter, state.duniterNodes, context),
+              _buildNodeInfo(
+                  NodeType.cesiumPlus, state.cesiumPlusNodes, context),
+              LinkCard(
+                  title: 'code_card_title',
+                  icon: Icons.code_rounded,
+                  url: Uri.parse('https://git.duniter.org/vjrj/ginkgo')),
               const BottomWidget()
             ]),
       );
     });
   }
+
+  GestureDetector _buildNodeInfo(
+      NodeType type, List<Node> nodes, BuildContext context) {
+    return GestureDetector(
+        onTap: () => showTooltip(context, '', tr('long_press_to_refresh')),
+        onLongPress: () {
+          logger('On long press');
+          if (type == NodeType.duniter) {
+            fetchDuniterNodes(force: true);
+          } else {
+            fetchCesiumPlusNodes(force: true);
+          }
+        },
+        child: InfoCard(
+            title: tr('using_nodes', namedArgs: <String, String>{
+              'type': type.name,
+              'nodes': nodes.length.toString()
+            }),
+            translate: false,
+            subtitle: nodes.isNotEmpty
+                ? tr('using_nodes_first',
+                    namedArgs: <String, String>{'node': nodes.first.url})
+                : '',
+            icon: Icons.hub));
+  }
 }
diff --git a/lib/ui/widgets/first_screen/pay_contact_search_dialog.dart b/lib/ui/widgets/first_screen/pay_contact_search_dialog.dart
index 28ae3f01..e58bda9e 100644
--- a/lib/ui/widgets/first_screen/pay_contact_search_dialog.dart
+++ b/lib/ui/widgets/first_screen/pay_contact_search_dialog.dart
@@ -44,9 +44,8 @@ class _SearchDialogState extends State<SearchDialog> {
     final Response response = await searchUser(_searchTerm);
     if (response.statusCode == 404) {
       _results = <Contact>[];
-      _isLoading = false;
       if (validateKey(_searchTerm)) {
-        // looks like a plain key
+        // looks like a plain pub key
         final Contact contact = Contact(pubkey: _searchTerm);
         _results.add(contact);
       }
@@ -62,6 +61,7 @@ class _SearchDialogState extends State<SearchDialog> {
         logger('Contact retrieved in search $c');
         return c;
       }).toList();
+      logger('Found: ${_results.length}');
       setState(() {
         _isLoading = false;
       });
-- 
GitLab