From fffcd5f4564a7e2578bbcea1d2f34182abc19760 Mon Sep 17 00:00:00 2001
From: vjrj <vjrj@comunes.org>
Date: Sat, 18 Mar 2023 08:31:08 +0100
Subject: [PATCH] Third hackaton

---
 README.md                                     |   2 +-
 assets/.env.development                       |  15 +-
 assets/env.production.txt                     |  15 +-
 assets/img/favicon.png                        | Bin 12142 -> 12142 bytes
 assets/img/favicon.sh                         |   1 +
 assets/translations/en.json                   |  42 +-
 assets/translations/es.json                   |  41 +-
 assets/translations/fr.json                   |  34 +-
 lib/data/models/app_cubit.dart                |   2 -
 lib/data/models/app_state.dart                |   2 +-
 lib/data/models/contact.dart                  |  50 +++
 lib/data/models/contact.g.dart                | 120 ++++++
 lib/data/models/contact_cubit.dart            |  85 ++++
 lib/data/models/contact_state.dart            |  29 ++
 ...lizable.dart => is_json_serializable.dart} |   0
 lib/data/models/model_utils.dart              |   6 +
 lib/data/models/node.dart                     |  13 +-
 lib/data/models/node_list_cubit.dart          |  13 +-
 lib/data/models/node_list_state.dart          |   8 +-
 lib/data/models/node_list_state.g.dart        |  10 +-
 lib/data/models/payment_cubit.dart            |  17 +-
 lib/data/models/payment_state.dart            |  23 +-
 lib/data/models/payment_state.g.dart          |  12 +-
 lib/data/models/transaction.dart              |  75 ++++
 lib/data/models/transaction.g.dart            | 268 +++++++++++++
 lib/data/models/transaction_cubit.dart        |  68 ++++
 lib/g1/api.dart                               | 130 +++++--
 lib/g1/g1_helper.dart                         |  55 ++-
 lib/g1/transaction.dart                       |  73 ----
 lib/g1/transaction_parser.dart                |  29 +-
 lib/main.dart                                 | 208 +++++-----
 lib/ui/screens/fifth_screen.dart              |  35 +-
 lib/ui/screens/first_screen.dart              |  86 ++--
 lib/ui/screens/skeleton_screen.dart           |  34 +-
 lib/ui/screens/third_screen.dart              |   7 +-
 lib/ui/ui_helpers.dart                        |  18 +-
 lib/ui/widgets/bottom_widget.dart             |  10 +
 lib/ui/widgets/fifth_screen/info_card.dart    |   3 +
 lib/ui/widgets/first_screen/credit_card.dart  |  43 +-
 .../pay_contact_search_dialog.dart            | 312 +++++++++------
 .../first_screen/recipient_widget.dart        |   9 +-
 .../widgets/fourth_screen/balance_chart.dart  |   2 +-
 .../fourth_screen/transaction_chart.dart      | 366 ++++++++++++++++++
 .../fourth_screen/transaction_page.dart       | 321 ++++++++-------
 lib/ui/widgets/header.dart                    |   5 +-
 lib/ui/widgets/loading_box.dart               |  13 +
 .../second_screen/card_terminal_screen.dart   |  20 +-
 .../second_screen/card_terminal_status.dart   |   2 +-
 .../widgets/third_screen/contacts_page.dart   | 173 +++++++++
 pubspec.lock                                  |  64 +++
 pubspec.yaml                                  |   5 +
 test/keys_tests.dart                          |  30 +-
 test/transactions_tests.dart                  |   4 +-
 53 files changed, 2324 insertions(+), 684 deletions(-)
 create mode 100644 lib/data/models/contact.dart
 create mode 100644 lib/data/models/contact.g.dart
 create mode 100644 lib/data/models/contact_cubit.dart
 create mode 100644 lib/data/models/contact_state.dart
 rename lib/data/models/{isJsonSerializable.dart => is_json_serializable.dart} (100%)
 create mode 100644 lib/data/models/model_utils.dart
 create mode 100644 lib/data/models/transaction.dart
 create mode 100644 lib/data/models/transaction.g.dart
 create mode 100644 lib/data/models/transaction_cubit.dart
 delete mode 100644 lib/g1/transaction.dart
 create mode 100644 lib/ui/widgets/bottom_widget.dart
 create mode 100644 lib/ui/widgets/fourth_screen/transaction_chart.dart
 create mode 100644 lib/ui/widgets/third_screen/contacts_page.dart

diff --git a/README.md b/README.md
index f5960fec..28e5e3c5 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,6 @@ users to manage their Äž1 currency on their mobile device using just a browser.
   amounts (which lightweight wallets will understand).
 * Internationalization (i18n)
 * Some contextual help (for example, by tapping on "Validity").
-* Connectivity detection (to retry transactions)
 * QR code reader
 
 ## Work in progress
@@ -23,6 +22,7 @@ users to manage their Äž1 currency on their mobile device using just a browser.
 * Send and receive Äž1 transactions
 * View transaction history
 * View Äž1 balance and currency conversion rate
+* Connectivity detection (to retry transactions, and other net operations)
 
 ## Demo
 
diff --git a/assets/.env.development b/assets/.env.development
index 4f3c830b..36d4e49d 100644
--- a/assets/.env.development
+++ b/assets/.env.development
@@ -1 +1,14 @@
-NET=https://g1.duniter.org/
+# Sentry is not used right now in development
+SENTRY_DSN=https://306345cb87ee4e1cbbe9023fb4afc5fc@sentry.comunes.org/6
+
+# Card customization
+CARD_COLOR_LEFT=0xFF598040
+CARD_COLOR_RIGHT=0xFF225500
+# Empty for default
+CARD_COLOR_TEXT=Äž1 Wallet Dev
+
+# Nodes space-separated
+# 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.e-is.pro https://g1.data.presler.fr https://g1.data.le-sou.org https://g1.data.mithril.r
diff --git a/assets/env.production.txt b/assets/env.production.txt
index 4f3c830b..a1fa2b6d 100644
--- a/assets/env.production.txt
+++ b/assets/env.production.txt
@@ -1 +1,14 @@
-NET=https://g1.duniter.org/
+SENTRY_DSN=https://306345cb87ee4e1cbbe9023fb4afc5fc@sentry.comunes.org/6
+
+# Card customization
+CARD_COLOR_LEFT=0xFF05112B
+CARD_COLOR_RIGHT=0xFF085476
+# Empty for default
+CARD_COLOR_TEXT=Äž1 Wallet Cop
+
+# Nodes space-separated
+# 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.e-is.pro https://g1.data.presler.fr https://g1.data.le-sou.org https://g1.data.mithril.r
+
diff --git a/assets/img/favicon.png b/assets/img/favicon.png
index bbb163d637e14d566d5592d4843c796358eec03d..bc6aa05607752193de7656dc5420aa5d5a12069a 100644
GIT binary patch
delta 19
bcmaDC_bzTiKL;;^x#ogb(&sl$zpe)WSzHM2

delta 19
bcmaDC_bzTiKL@*zu>Og<6KWf$U)KWwSG5S7

diff --git a/assets/img/favicon.sh b/assets/img/favicon.sh
index 03f29275..9ad1b60d 100755
--- a/assets/img/favicon.sh
+++ b/assets/img/favicon.sh
@@ -9,6 +9,7 @@ convert leaf.png -resize 256x256 \
 convert leaf.png -resize 256x256 favicon.png
 
 cp favicon.png ../../web/
+cp favicon.ico ../../web/
 
 for i in 192 512; do convert leaf.png -resize $ix$i ../../web/icons/Icon-$i.png; done
 for i in 192 512; do convert leaf.png -resize $ix$i ../../web/icons/Icon-maskable-$i.png; done
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 72042806..0b9b64f0 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -8,7 +8,7 @@
   "bottom_nav_trd": "Contacts",
   "bottom_nav_frd": "Balance",
   "bottom_nav_fifth": "Info",
-  "title_first": "Send Äž1",
+  "send_g1": "Send Äž1",
   "g1_amount": "Amount to send",
   "g1_amount_hint": "Amount to send in Äž1",
   "g1_form_pay_send": "Send",
@@ -32,22 +32,34 @@
   "skip": "Skip",
   "start": "Start",
   "offline": "You are Offline!",
-  "online-terminal": "Online",
-  "offline-terminal": "Offline",
-  "show-qr-to-client": "Show your public key to your client",
-  "show-qr-to-client-amount": "Show your QR with this amount",
-  "keys-tooltip": "Public and private keys in Äž1 and Duniter are like a lock and key system, where the public key acts as the lock that can be opened by anyone with the corresponding private key, providing a secure way to authenticate and verify transactions",
-  "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",
-  "key-copied-to-clipboard": "Your public key has been copied to the clipboard",
+  "online_terminal": "Online",
+  "offline_terminal": "Offline",
+  "show_qr_to_client": "Show your public key to your client",
+  "show_qr_to_client_amount": "Show your QR with this amount",
+  "keys_tooltip": "Public and private keys in Äž1 and Duniter are like a lock and key system, where the public key acts as the lock that can be opened by anyone with the corresponding private key, providing a secure way to authenticate and verify transactions",
+  "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",
+  "key_copied_to_clipboard": "Your public key has been copied to the clipboard",
+  "some_key_copied_to_clipboard": "The public key has been copied to the clipboard",
   "key_tools_title": "Keys and Tools",
   "transactions": "Transactions",
   "balance": "Balance",
   "transaction_from_to": "From {from} to {to}",
-  "your_wallet": "your wallet"
+  "your_wallet": "your wallet",
+  "delete_contact": "Delete contact",
+  "search_contacts": "Search contacts",
+  "no_contacts": "You don't have contacts yet",
+  "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"
 }
diff --git a/assets/translations/es.json b/assets/translations/es.json
index 71d7a916..c0e01c43 100644
--- a/assets/translations/es.json
+++ b/assets/translations/es.json
@@ -8,7 +8,7 @@
   "bottom_nav_trd": "Contactos",
   "bottom_nav_frd": "Saldo",
   "bottom_nav_fifth": "Información",
-  "title_first": "Enviar Äž1",
+  "send_g1": "Enviar Äž1",
   "g1_amount": "Monto a enviar",
   "g1_amount_hint": "Monto a enviar en Äž1",
   "g1_form_pay_send": "Enviar",
@@ -32,22 +32,33 @@
   "skip": "Omitir",
   "start": "Comenzar",
   "offline": "¡Está desconectad@!",
-  "online-terminal": "En línea",
-  "offline-terminal": "Fuera de línea",
-  "show-qr-to-client": "Muestre su clave pública a su clientæ",
-  "show-qr-to-client-amount": "Muestre su QR con esa cantidad",
-  "keys-tooltip": "Las claves públicas y privadas en Ğ1 y Duniter son como un sistema de cerradura y llave, donde la clave pública actúa como la cerradura que puede ser abierta por cualquiera que tenga la clave privada correspondiente, proporcionando una forma segura de autenticar y verificar transacciones.",
-  "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",
-  "key-copied-to-clipboard": "Tu clave pública se ha copiado al portapapeles",
+  "online_terminal": "En línea",
+  "offline_terminal": "Fuera de línea",
+  "show_qr_to_client": "Muestre su clave pública a su clientæ",
+  "show_qr_to_client_amount": "Muestre su QR con esa cantidad",
+  "keys_tooltip": "Las claves públicas y privadas en Ğ1 y Duniter son como un sistema de cerradura y llave, donde la clave pública actúa como la cerradura que puede ser abierta por cualquiera que tenga la clave privada correspondiente, proporcionando una forma segura de autenticar y verificar transacciones.",
+  "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",
+  "key_copied_to_clipboard": "Tu clave pública se ha copiado al portapapeles",
+  "some_key_copied_to_clipboard": "La clave pública se ha copiado al portapapeles",
   "key_tools_title": "Llaves y Herramientas",
   "transactions": "Transacciones",
   "balance": "Balance",
   "transaction_from_to": "Desde {from} a {to}",
-  "your_wallet": "tu monedero"
+  "your_wallet": "tu monedero",
+  "delete_contact": "Borrar contacto",
+  "search_contacts": "Buscar contactos",
+  "no_contacts": "No tiene contactos aún",
+  "data_load_error": "Error al cargar los datos",
+  "add_contact": "Añadir contacto",
+  "contact_added": "Contacto añadido",
+  "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"
 }
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
index 3d58581d..0780bd5b 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",
-  "title_first": "Envoyer des Äž1",
+  "send_g1: "Envoyer des Äž1",
   "g1_amount": "Montant à envoyer",
   "g1_amount_hint": "Montant à envoyer en Ğ1",
   "g1_form_pay_send": "Envoyer",
@@ -32,22 +32,26 @@
   "skip": "Passer",
   "start": "Commencer",
   "offline": "Vous êtes hors ligne !",
-  "online-terminal": "En ligne",
-  "offline-terminal": "Hors ligne",
-  "show-qr-to-client": "Montrez votre clé publique à votre client",
-  "show-qr-to-client-amount": "Montrez votre QR avec ce montant",
-  "keys-tooltip": "Les clés publiques et privées en Ğ1 et Duniter fonctionnent comme un système de verrou et de clé, où la clé publique agit comme le verrou qui peut être ouvert par n'importe qui ayant la clé privée correspondante, offrant ainsi un moyen sécurisé d'authentifier et de vérifier les transactions",
-  "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",
-  "key-copied-to-clipboard": "Votre clé publique a été copiée dans le presse-papiers",
+  "online_terminal": "En ligne",
+  "offline_terminal": "Hors ligne",
+  "show_qr_to_client": "Montrez votre clé publique à votre client",
+  "show_qr_to_client_amount": "Montrez votre QR avec ce montant",
+  "keys_tooltip": "Les clés publiques et privées en Ğ1 et Duniter fonctionnent comme un système de verrou et de clé, où la clé publique agit comme le verrou qui peut être ouvert par n'importe qui ayant la clé privée correspondante, offrant ainsi un moyen sécurisé d'authentifier et de vérifier les transactions",
+  "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",
+  "key_copied_to_clipboard": "Votre clé publique a été copiée dans le presse-papiers",
   "key_tools_title": "Clés et outils",
   "transactions": "Transactions",
   "balance": "Solde",
   "transaction_from_to": "De {from} à {to}",
-  "your_wallet": "votre portefeuille"
+  "your_wallet": "votre portefeuille",
+  "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"
 }
diff --git a/lib/data/models/app_cubit.dart b/lib/data/models/app_cubit.dart
index e58d25d3..2913ed15 100644
--- a/lib/data/models/app_cubit.dart
+++ b/lib/data/models/app_cubit.dart
@@ -26,6 +26,4 @@ class AppCubit extends HydratedCubit<AppState> {
   Map<String, dynamic> toJson(AppState state) {
     return state.toJson();
   }
-  
 }
-
diff --git a/lib/data/models/app_state.dart b/lib/data/models/app_state.dart
index e7f1a039..ee21fe39 100644
--- a/lib/data/models/app_state.dart
+++ b/lib/data/models/app_state.dart
@@ -1,7 +1,7 @@
 import 'package:equatable/equatable.dart';
 import 'package:json_annotation/json_annotation.dart';
 
-import 'isJsonSerializable.dart';
+import 'is_json_serializable.dart';
 
 part 'app_state.g.dart';
 
diff --git a/lib/data/models/contact.dart b/lib/data/models/contact.dart
new file mode 100644
index 00000000..8c684d81
--- /dev/null
+++ b/lib/data/models/contact.dart
@@ -0,0 +1,50 @@
+import 'dart:typed_data';
+
+import 'package:copy_with_extension/copy_with_extension.dart';
+import 'package:equatable/equatable.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+import 'is_json_serializable.dart';
+
+part 'contact.g.dart';
+
+@JsonSerializable()
+@CopyWith()
+class Contact extends Equatable implements IsJsonSerializable<Contact> {
+  const Contact({
+    this.nick,
+    required this.pubkey,
+    this.avatar,
+    this.notes,
+    this.name,
+  });
+
+  factory Contact.fromJson(Map<String, dynamic> json) =>
+      _$ContactFromJson(json);
+
+  final String? nick;
+  final String pubkey;
+  @JsonKey(fromJson: _fromList, toJson: _toList)
+  final Uint8List? avatar;
+  final String? notes;
+  final String? name;
+
+  @override
+  List<Object?> get props => <dynamic>[nick, pubkey, avatar, notes, name];
+
+  @override
+  Map<String, dynamic> toJson() => _$ContactToJson(this);
+
+  @override
+  Contact fromJson(Map<String, dynamic> json) => Contact.fromJson(json);
+
+  static Uint8List _fromList(List<int> list) => Uint8List.fromList(list);
+
+  static List<int> _toList(Uint8List? uint8List) =>
+      uint8List != null ? uint8List.toList() : <int>[];
+
+  @override
+  String toString() {
+    return 'Contact $pubkey, hasAvatar: ${avatar != null}, nick: $nick, name: $name';
+  }
+}
diff --git a/lib/data/models/contact.g.dart b/lib/data/models/contact.g.dart
new file mode 100644
index 00000000..e5bc557c
--- /dev/null
+++ b/lib/data/models/contact.g.dart
@@ -0,0 +1,120 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'contact.dart';
+
+// **************************************************************************
+// CopyWithGenerator
+// **************************************************************************
+
+abstract class _$ContactCWProxy {
+  Contact nick(String? nick);
+
+  Contact pubkey(String pubkey);
+
+  Contact avatar(Uint8List? avatar);
+
+  Contact notes(String? notes);
+
+  Contact name(String? name);
+
+  /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Contact(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
+  ///
+  /// Usage
+  /// ```dart
+  /// Contact(...).copyWith(id: 12, name: "My name")
+  /// ````
+  Contact call({
+    String? nick,
+    String? pubkey,
+    Uint8List? avatar,
+    String? notes,
+    String? name,
+  });
+}
+
+/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfContact.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfContact.copyWith.fieldName(...)`
+class _$ContactCWProxyImpl implements _$ContactCWProxy {
+  const _$ContactCWProxyImpl(this._value);
+
+  final Contact _value;
+
+  @override
+  Contact nick(String? nick) => this(nick: nick);
+
+  @override
+  Contact pubkey(String pubkey) => this(pubkey: pubkey);
+
+  @override
+  Contact avatar(Uint8List? avatar) => this(avatar: avatar);
+
+  @override
+  Contact notes(String? notes) => this(notes: notes);
+
+  @override
+  Contact name(String? name) => this(name: name);
+
+  @override
+
+  /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Contact(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
+  ///
+  /// Usage
+  /// ```dart
+  /// Contact(...).copyWith(id: 12, name: "My name")
+  /// ````
+  Contact call({
+    Object? nick = const $CopyWithPlaceholder(),
+    Object? pubkey = const $CopyWithPlaceholder(),
+    Object? avatar = const $CopyWithPlaceholder(),
+    Object? notes = const $CopyWithPlaceholder(),
+    Object? name = const $CopyWithPlaceholder(),
+  }) {
+    return Contact(
+      nick: nick == const $CopyWithPlaceholder()
+          ? _value.nick
+          // ignore: cast_nullable_to_non_nullable
+          : nick as String?,
+      pubkey: pubkey == const $CopyWithPlaceholder() || pubkey == null
+          ? _value.pubkey
+          // ignore: cast_nullable_to_non_nullable
+          : pubkey as String,
+      avatar: avatar == const $CopyWithPlaceholder()
+          ? _value.avatar
+          // ignore: cast_nullable_to_non_nullable
+          : avatar as Uint8List?,
+      notes: notes == const $CopyWithPlaceholder()
+          ? _value.notes
+          // ignore: cast_nullable_to_non_nullable
+          : notes as String?,
+      name: name == const $CopyWithPlaceholder()
+          ? _value.name
+          // ignore: cast_nullable_to_non_nullable
+          : name as String?,
+    );
+  }
+}
+
+extension $ContactCopyWith on Contact {
+  /// Returns a callable class that can be used as follows: `instanceOfContact.copyWith(...)` or like so:`instanceOfContact.copyWith.fieldName(...)`.
+  // ignore: library_private_types_in_public_api
+  _$ContactCWProxy get copyWith => _$ContactCWProxyImpl(this);
+}
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+Contact _$ContactFromJson(Map<String, dynamic> json) => Contact(
+      nick: json['nick'] as String?,
+      pubkey: json['pubkey'] as String,
+      avatar: Contact._fromList(json['avatar'] as List<int>),
+      notes: json['notes'] as String?,
+      name: json['name'] as String?,
+    );
+
+Map<String, dynamic> _$ContactToJson(Contact instance) => <String, dynamic>{
+      'nick': instance.nick,
+      'pubkey': instance.pubkey,
+      'avatar': Contact._toList(instance.avatar),
+      'notes': instance.notes,
+      'name': instance.name,
+    };
diff --git a/lib/data/models/contact_cubit.dart b/lib/data/models/contact_cubit.dart
new file mode 100644
index 00000000..e162f7d5
--- /dev/null
+++ b/lib/data/models/contact_cubit.dart
@@ -0,0 +1,85 @@
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'contact.dart';
+import 'contact_state.dart';
+
+class ContactsCubit extends HydratedCubit<ContactsState> {
+  ContactsCubit() : super(const ContactsState());
+
+  void addContact(Contact contact) {
+    if (!state.contacts.contains(contact)) {
+      emit(state.copyWith(contacts: <Contact>[...state.contacts, contact]));
+    }
+  }
+
+  void removeContact(Contact contact) {
+    final List<Contact> contactsTruncated = state.contacts
+        .where((Contact c) => c.pubkey != contact.pubkey)
+        .toList();
+    final List<Contact> filteredContactsTruncated = state.filteredContacts
+        .where((Contact c) => c.pubkey != contact.pubkey)
+        .toList();
+    emit(state.copyWith(
+        contacts: contactsTruncated,
+        filteredContacts: filteredContactsTruncated));
+  }
+
+  void updateContact(Contact contact) {
+    final List<Contact> contacts = state.contacts.map((Contact c) {
+      if (c.pubkey == contact.pubkey) {
+        return contact;
+      }
+      return c;
+    }).toList();
+    emit(state.copyWith(contacts: contacts));
+  }
+
+  void resetFilter() {
+    emit(state.copyWith(filteredContacts: state.contacts));
+  }
+
+  void filterContacts(String query) {
+    final List<Contact> contacts = state.contacts.where((Contact c) {
+      if (c.pubkey.contains(query)) {
+        return true;
+      }
+      if (c.nick != null && c.nick!.contains(query)) {
+        return true;
+      }
+      if (c.name != null && c.name!.contains(query)) {
+        return true;
+      }
+      if (c.notes != null && c.notes!.contains(query)) {
+        return true;
+      }
+      return false;
+    }).toList();
+    emit(state.copyWith(filteredContacts: contacts));
+  }
+
+  List<Contact> get contacts => state.contacts;
+
+  List<Contact> get filteredContacts => state.filteredContacts;
+
+  @override
+  ContactsState fromJson(Map<String, dynamic> json) {
+    final List<dynamic> contactsJson = json['contacts'] as List<dynamic>;
+    final List<Contact> contacts = contactsJson
+        .map((dynamic c) => Contact.fromJson(c as Map<String, dynamic>))
+        .toList();
+    return ContactsState(contacts: contacts);
+  }
+
+  @override
+  Map<String, dynamic> toJson(ContactsState state) {
+    final List<Map<String, dynamic>> contactsJson =
+        state.contacts.map((Contact c) => c.toJson()).toList();
+    return <String, dynamic>{'contacts': contactsJson};
+  }
+
+  @override
+  String get id => 'contacts';
+
+  bool isContact(String pubKey) =>
+      state.contacts.any((Contact c) => c.pubkey == pubKey);
+}
diff --git a/lib/data/models/contact_state.dart b/lib/data/models/contact_state.dart
new file mode 100644
index 00000000..c519574b
--- /dev/null
+++ b/lib/data/models/contact_state.dart
@@ -0,0 +1,29 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+
+import 'contact.dart';
+
+@immutable
+class ContactsState extends Equatable {
+  const ContactsState(
+      {this.contacts = const <Contact>[],
+      this.filteredContacts = const <Contact>[]});
+
+  final List<Contact> contacts;
+  final List<Contact> filteredContacts;
+
+  @override
+  List<Object?> get props => <Object>[contacts, filteredContacts];
+
+  ContactsState copyWith(
+      {List<Contact>? contacts, List<Contact>? filteredContacts}) {
+    return ContactsState(
+        contacts: contacts ?? this.contacts,
+        filteredContacts: filteredContacts ?? this.filteredContacts);
+  }
+
+  @override
+  String toString() {
+    return 'ContactsState(contacts: $contacts, filteredContacts: $filteredContacts)';
+  }
+}
diff --git a/lib/data/models/isJsonSerializable.dart b/lib/data/models/is_json_serializable.dart
similarity index 100%
rename from lib/data/models/isJsonSerializable.dart
rename to lib/data/models/is_json_serializable.dart
diff --git a/lib/data/models/model_utils.dart b/lib/data/models/model_utils.dart
new file mode 100644
index 00000000..57d0d80e
--- /dev/null
+++ b/lib/data/models/model_utils.dart
@@ -0,0 +1,6 @@
+import 'dart:typed_data';
+
+Uint8List uIntFromList(List<int> list) => Uint8List.fromList(list);
+
+List<int> uIntToList(Uint8List? uInt8List) =>
+    uInt8List != null ? uInt8List.toList() : <int>[];
diff --git a/lib/data/models/node.dart b/lib/data/models/node.dart
index be019e27..2a0da074 100644
--- a/lib/data/models/node.dart
+++ b/lib/data/models/node.dart
@@ -1,7 +1,8 @@
 import 'package:equatable/equatable.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
 import 'package:json_annotation/json_annotation.dart';
 
-import 'isJsonSerializable.dart';
+import 'is_json_serializable.dart';
 
 part 'node.g.dart';
 
@@ -46,7 +47,13 @@ class Node extends Equatable implements IsJsonSerializable<Node> {
   List<Object?> get props => <dynamic>[url, latency, errors];
 }
 
-const List<Node> defaultDuniterNodes = <Node>[
+List<Node> readDotNodeConfig(String entry) =>
+    dotenv.env[entry]!.split(' ').map((String url) => Node(url: url)).toList();
+
+List<Node> defaultDuniterNodes = readDotNodeConfig('DUNITER_NODES');
+List<Node> defaultCesiumPlusNodes = readDotNodeConfig('CESIUM_PLUS_NODES');
+
+const List<Node> defaultDuniterNodesRemove = <Node>[
   Node(url: 'https://g1.duniter.fr'),
   Node(url: 'https://g1.le-sou.org'),
   Node(url: 'https://g1.cgeek.fr'),
@@ -56,7 +63,7 @@ const List<Node> defaultDuniterNodes = <Node>[
   Node(url: 'https://g1.cgeek.fr')
 ];
 
-const List<Node> defaultCesiumPlusNodes = <Node>[
+const List<Node> defaultCesiumPlusNodesRemove = <Node>[
   Node(url: 'https://g1.data.e-is.pro'),
   Node(url: 'https://g1.data.presler.fr'),
   Node(url: 'https://g1.data.le-sou.org'),
diff --git a/lib/data/models/node_list_cubit.dart b/lib/data/models/node_list_cubit.dart
index 045e28a7..28a8b3d2 100644
--- a/lib/data/models/node_list_cubit.dart
+++ b/lib/data/models/node_list_cubit.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/foundation.dart';
 import 'package:hydrated_bloc/hydrated_bloc.dart';
 
 import 'node.dart';
@@ -6,9 +7,11 @@ import 'node_list_state.dart';
 class NodeListCubit extends HydratedCubit<NodeListState> {
   NodeListCubit() : super(NodeListState());
 
+  static int maxNodes = kReleaseMode ? 20 : 5;
+  static int maxNodeErrors = 3;
+
   void addDuniterNode(Node node) {
-    final Node? nFound = _find(node);
-    if (nFound == null) {
+    if (!_find(node)) {
       // Does not exists, so add it
       emit(state.copyWith(duniterNodes: <Node>[...state.duniterNodes, node]));
     } else {
@@ -17,12 +20,10 @@ class NodeListCubit extends HydratedCubit<NodeListState> {
     }
   }
 
-  Node? _find(Node node) =>
-      state.duniterNodes.firstWhere((Node n) => n.url == node.url);
+  bool _find(Node node) => state.duniterNodes.contains(node);
 
   void insertDuniterNode(Node node) {
-    final Node? nFound = _find(node);
-    if (nFound == null) {
+    if (!_find(node)) {
       emit(state.copyWith(duniterNodes: <Node>[node, ...state.duniterNodes]));
     } else {
       // it exists
diff --git a/lib/data/models/node_list_state.dart b/lib/data/models/node_list_state.dart
index 04d048e2..1aca7c7a 100644
--- a/lib/data/models/node_list_state.dart
+++ b/lib/data/models/node_list_state.dart
@@ -10,10 +10,12 @@ part 'node_list_state.g.dart';
 @JsonSerializable()
 class NodeListState extends Equatable {
   NodeListState(
-      {this.duniterNodes = defaultDuniterNodes,
-      this.cesiumPlusNodes = defaultCesiumPlusNodes,
+      {List<Node>? duniterNodes,
+      List<Node>? cesiumPlusNodes,
       DateTime? lastFetchNodesTime})
-      : lastFetchNodesTime = lastFetchNodesTime ?? DateTime(1970);
+      : duniterNodes = duniterNodes ?? defaultDuniterNodes,
+        cesiumPlusNodes = cesiumPlusNodes ?? defaultCesiumPlusNodes,
+        lastFetchNodesTime = lastFetchNodesTime ?? DateTime(1970);
 
   factory NodeListState.fromJson(Map<String, dynamic> json) =>
       _$NodeListStateFromJson(json);
diff --git a/lib/data/models/node_list_state.g.dart b/lib/data/models/node_list_state.g.dart
index a74d940a..d90bf061 100644
--- a/lib/data/models/node_list_state.g.dart
+++ b/lib/data/models/node_list_state.g.dart
@@ -9,13 +9,11 @@ part of 'node_list_state.dart';
 NodeListState _$NodeListStateFromJson(Map<String, dynamic> json) =>
     NodeListState(
       duniterNodes: (json['duniterNodes'] as List<dynamic>?)
-              ?.map((e) => Node.fromJson(e as Map<String, dynamic>))
-              .toList() ??
-          defaultDuniterNodes,
+          ?.map((e) => Node.fromJson(e as Map<String, dynamic>))
+          .toList(),
       cesiumPlusNodes: (json['cesiumPlusNodes'] as List<dynamic>?)
-              ?.map((e) => Node.fromJson(e as Map<String, dynamic>))
-              .toList() ??
-          defaultCesiumPlusNodes,
+          ?.map((e) => Node.fromJson(e as Map<String, dynamic>))
+          .toList(),
       lastFetchNodesTime: json['lastFetchNodesTime'] == null
           ? null
           : DateTime.parse(json['lastFetchNodesTime'] as String),
diff --git a/lib/data/models/payment_cubit.dart b/lib/data/models/payment_cubit.dart
index 86b4ecff..b05d79a1 100644
--- a/lib/data/models/payment_cubit.dart
+++ b/lib/data/models/payment_cubit.dart
@@ -20,8 +20,8 @@ class PaymentCubit extends HydratedCubit<PaymentState> {
     emit(newState);
   }
 
-  void selectUser(String publicKey, String nick, Uint8List avatar) {
-    final PaymentState newState = state.copyWith(
+  void selectUser(String publicKey, String? nick, Uint8List? avatar) {
+    final PaymentState newState = PaymentState(
       publicKey: publicKey,
       nick: nick,
       avatar: avatar,
@@ -29,10 +29,17 @@ class PaymentCubit extends HydratedCubit<PaymentState> {
     emit(newState);
   }
 
+  void selectKeyAmount(String publicKey, double amount) {
+    final PaymentState newState =
+        PaymentState(publicKey: publicKey, amount: amount);
+    emit(newState);
+  }
+
   void selectKey(String publicKey) {
-    final PaymentState newState = state.copyWith(
-      publicKey: publicKey,
-    );
+    final PaymentState newState = PaymentState(
+        publicKey: publicKey,
+        amount: state.amount,
+        description: state.description);
     emit(newState);
   }
 
diff --git a/lib/data/models/payment_state.dart b/lib/data/models/payment_state.dart
index cb8450b5..d82b6e70 100644
--- a/lib/data/models/payment_state.dart
+++ b/lib/data/models/payment_state.dart
@@ -3,28 +3,30 @@ import 'dart:typed_data';
 import 'package:equatable/equatable.dart';
 import 'package:json_annotation/json_annotation.dart';
 
+import 'model_utils.dart';
+
 part 'payment_state.g.dart';
 
 @JsonSerializable()
 class PaymentState extends Equatable {
   const PaymentState({
     required this.publicKey,
-    required this.nick,
+    this.nick,
     this.avatar,
-    required this.description,
-    required this.amount,
-    required this.isSent,
+    this.description = '',
+    this.amount,
+    this.isSent = false,
   });
 
   factory PaymentState.fromJson(Map<String, dynamic> json) =>
       _$PaymentStateFromJson(json);
 
   final String publicKey;
-  final String nick;
-  @JsonKey(fromJson: _fromList, toJson: _toList)
+  final String? nick;
+  @JsonKey(fromJson: uIntFromList, toJson: uIntToList)
   final Uint8List? avatar;
   final String description;
-  final double amount;
+  final double? amount;
   final bool isSent;
 
   Map<String, dynamic> toJson() => _$PaymentStateToJson(this);
@@ -50,17 +52,10 @@ class PaymentState extends Equatable {
   static PaymentState emptyPayment = const PaymentState(
     publicKey: '',
     nick: '',
-    description: '',
     amount: 0,
-    isSent: false,
   );
 
   @override
   List<Object?> get props =>
       <dynamic>[publicKey, nick, avatar, description, amount, isSent];
-
-  static Uint8List _fromList(List<int> list) => Uint8List.fromList(list);
-
-  static List<int> _toList(Uint8List? uint8List) =>
-      uint8List != null ? uint8List.toList() : <int>[];
 }
diff --git a/lib/data/models/payment_state.g.dart b/lib/data/models/payment_state.g.dart
index a2412852..c8dd3a37 100644
--- a/lib/data/models/payment_state.g.dart
+++ b/lib/data/models/payment_state.g.dart
@@ -8,18 +8,18 @@ part of 'payment_state.dart';
 
 PaymentState _$PaymentStateFromJson(Map<String, dynamic> json) => PaymentState(
       publicKey: json['publicKey'] as String,
-      nick: json['nick'] as String,
-      avatar: PaymentState._fromList(json['avatar'] as List<int>),
-      description: json['description'] as String,
-      amount: (json['amount'] as num).toDouble(),
-      isSent: json['isSent'] as bool,
+      nick: json['nick'] as String?,
+      avatar: uIntFromList(json['avatar'] as List<int>),
+      description: json['description'] as String? ?? '',
+      amount: (json['amount'] as num?)?.toDouble(),
+      isSent: json['isSent'] as bool? ?? false,
     );
 
 Map<String, dynamic> _$PaymentStateToJson(PaymentState instance) =>
     <String, dynamic>{
       'publicKey': instance.publicKey,
       'nick': instance.nick,
-      'avatar': PaymentState._toList(instance.avatar),
+      'avatar': uIntToList(instance.avatar),
       'description': instance.description,
       'amount': instance.amount,
       'isSent': instance.isSent,
diff --git a/lib/data/models/transaction.dart b/lib/data/models/transaction.dart
new file mode 100644
index 00000000..4218a9e0
--- /dev/null
+++ b/lib/data/models/transaction.dart
@@ -0,0 +1,75 @@
+import 'dart:typed_data';
+
+import 'package:copy_with_extension/copy_with_extension.dart';
+import 'package:equatable/equatable.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+import 'model_utils.dart';
+
+part 'transaction.g.dart';
+
+@JsonSerializable()
+@CopyWith()
+class Transaction extends Equatable {
+  const Transaction({
+    required this.from,
+    required this.to,
+    required this.amount,
+    required this.comment,
+    required this.time,
+    this.toAvatar,
+    this.toNick,
+    this.fromAvatar,
+    this.fromNick,
+  });
+
+  factory Transaction.fromJson(Map<String, dynamic> json) =>
+      _$TransactionFromJson(json);
+  final String from;
+  final String to;
+  @JsonKey(fromJson: uIntFromList, toJson: uIntToList)
+  final Uint8List? toAvatar;
+  final String? toNick;
+  final int amount;
+  @JsonKey(fromJson: uIntFromList, toJson: uIntToList)
+  final Uint8List? fromAvatar;
+  final String? fromNick;
+  final String comment;
+  final DateTime time;
+
+  Map<String, dynamic> toJson() => _$TransactionToJson(this);
+
+  @override
+  List<Object?> get props => <dynamic>[
+        from,
+        to,
+        amount,
+        comment,
+        time,
+        toAvatar,
+        toNick,
+        fromAvatar,
+        fromNick
+      ];
+}
+
+@JsonSerializable()
+@CopyWith()
+class TransactionsAndBalanceState extends Equatable {
+  const TransactionsAndBalanceState({
+    required this.transactions,
+    required this.balance,
+    required this.lastChecked,
+  });
+
+  factory TransactionsAndBalanceState.fromJson(Map<String, dynamic> json) =>
+      _$TransactionsAndBalanceStateFromJson(json);
+  final List<Transaction> transactions;
+  final int balance;
+  final DateTime lastChecked;
+
+  Map<String, dynamic> toJson() => _$TransactionsAndBalanceStateToJson(this);
+
+  @override
+  List<Object?> get props => <dynamic>[transactions, balance, lastChecked];
+}
diff --git a/lib/data/models/transaction.g.dart b/lib/data/models/transaction.g.dart
new file mode 100644
index 00000000..992e707b
--- /dev/null
+++ b/lib/data/models/transaction.g.dart
@@ -0,0 +1,268 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'transaction.dart';
+
+// **************************************************************************
+// CopyWithGenerator
+// **************************************************************************
+
+abstract class _$TransactionCWProxy {
+  Transaction from(String from);
+
+  Transaction to(String to);
+
+  Transaction amount(int amount);
+
+  Transaction comment(String comment);
+
+  Transaction time(DateTime time);
+
+  Transaction toAvatar(Uint8List? toAvatar);
+
+  Transaction toNick(String? toNick);
+
+  Transaction fromAvatar(Uint8List? fromAvatar);
+
+  Transaction fromNick(String? fromNick);
+
+  /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Transaction(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
+  ///
+  /// Usage
+  /// ```dart
+  /// Transaction(...).copyWith(id: 12, name: "My name")
+  /// ````
+  Transaction call({
+    String? from,
+    String? to,
+    int? amount,
+    String? comment,
+    DateTime? time,
+    Uint8List? toAvatar,
+    String? toNick,
+    Uint8List? fromAvatar,
+    String? fromNick,
+  });
+}
+
+/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTransaction.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTransaction.copyWith.fieldName(...)`
+class _$TransactionCWProxyImpl implements _$TransactionCWProxy {
+  const _$TransactionCWProxyImpl(this._value);
+
+  final Transaction _value;
+
+  @override
+  Transaction from(String from) => this(from: from);
+
+  @override
+  Transaction to(String to) => this(to: to);
+
+  @override
+  Transaction amount(int amount) => this(amount: amount);
+
+  @override
+  Transaction comment(String comment) => this(comment: comment);
+
+  @override
+  Transaction time(DateTime time) => this(time: time);
+
+  @override
+  Transaction toAvatar(Uint8List? toAvatar) => this(toAvatar: toAvatar);
+
+  @override
+  Transaction toNick(String? toNick) => this(toNick: toNick);
+
+  @override
+  Transaction fromAvatar(Uint8List? fromAvatar) => this(fromAvatar: fromAvatar);
+
+  @override
+  Transaction fromNick(String? fromNick) => this(fromNick: fromNick);
+
+  @override
+
+  /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Transaction(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
+  ///
+  /// Usage
+  /// ```dart
+  /// Transaction(...).copyWith(id: 12, name: "My name")
+  /// ````
+  Transaction call({
+    Object? from = const $CopyWithPlaceholder(),
+    Object? to = const $CopyWithPlaceholder(),
+    Object? amount = const $CopyWithPlaceholder(),
+    Object? comment = const $CopyWithPlaceholder(),
+    Object? time = const $CopyWithPlaceholder(),
+    Object? toAvatar = const $CopyWithPlaceholder(),
+    Object? toNick = const $CopyWithPlaceholder(),
+    Object? fromAvatar = const $CopyWithPlaceholder(),
+    Object? fromNick = const $CopyWithPlaceholder(),
+  }) {
+    return Transaction(
+      from: from == const $CopyWithPlaceholder() || from == null
+          ? _value.from
+          // ignore: cast_nullable_to_non_nullable
+          : from as String,
+      to: to == const $CopyWithPlaceholder() || to == null
+          ? _value.to
+          // ignore: cast_nullable_to_non_nullable
+          : to as String,
+      amount: amount == const $CopyWithPlaceholder() || amount == null
+          ? _value.amount
+          // ignore: cast_nullable_to_non_nullable
+          : amount as int,
+      comment: comment == const $CopyWithPlaceholder() || comment == null
+          ? _value.comment
+          // ignore: cast_nullable_to_non_nullable
+          : comment as String,
+      time: time == const $CopyWithPlaceholder() || time == null
+          ? _value.time
+          // ignore: cast_nullable_to_non_nullable
+          : time as DateTime,
+      toAvatar: toAvatar == const $CopyWithPlaceholder()
+          ? _value.toAvatar
+          // ignore: cast_nullable_to_non_nullable
+          : toAvatar as Uint8List?,
+      toNick: toNick == const $CopyWithPlaceholder()
+          ? _value.toNick
+          // ignore: cast_nullable_to_non_nullable
+          : toNick as String?,
+      fromAvatar: fromAvatar == const $CopyWithPlaceholder()
+          ? _value.fromAvatar
+          // ignore: cast_nullable_to_non_nullable
+          : fromAvatar as Uint8List?,
+      fromNick: fromNick == const $CopyWithPlaceholder()
+          ? _value.fromNick
+          // ignore: cast_nullable_to_non_nullable
+          : fromNick as String?,
+    );
+  }
+}
+
+extension $TransactionCopyWith on Transaction {
+  /// Returns a callable class that can be used as follows: `instanceOfTransaction.copyWith(...)` or like so:`instanceOfTransaction.copyWith.fieldName(...)`.
+  // ignore: library_private_types_in_public_api
+  _$TransactionCWProxy get copyWith => _$TransactionCWProxyImpl(this);
+}
+
+abstract class _$TransactionsAndBalanceStateCWProxy {
+  TransactionsAndBalanceState transactions(List<Transaction> transactions);
+
+  TransactionsAndBalanceState balance(int balance);
+
+  TransactionsAndBalanceState lastChecked(DateTime lastChecked);
+
+  /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TransactionsAndBalanceState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
+  ///
+  /// Usage
+  /// ```dart
+  /// TransactionsAndBalanceState(...).copyWith(id: 12, name: "My name")
+  /// ````
+  TransactionsAndBalanceState call({
+    List<Transaction>? transactions,
+    int? balance,
+    DateTime? lastChecked,
+  });
+}
+
+/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTransactionsAndBalanceState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTransactionsAndBalanceState.copyWith.fieldName(...)`
+class _$TransactionsAndBalanceStateCWProxyImpl
+    implements _$TransactionsAndBalanceStateCWProxy {
+  const _$TransactionsAndBalanceStateCWProxyImpl(this._value);
+
+  final TransactionsAndBalanceState _value;
+
+  @override
+  TransactionsAndBalanceState transactions(List<Transaction> transactions) =>
+      this(transactions: transactions);
+
+  @override
+  TransactionsAndBalanceState balance(int balance) => this(balance: balance);
+
+  @override
+  TransactionsAndBalanceState lastChecked(DateTime lastChecked) =>
+      this(lastChecked: lastChecked);
+
+  @override
+
+  /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TransactionsAndBalanceState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
+  ///
+  /// Usage
+  /// ```dart
+  /// TransactionsAndBalanceState(...).copyWith(id: 12, name: "My name")
+  /// ````
+  TransactionsAndBalanceState call({
+    Object? transactions = const $CopyWithPlaceholder(),
+    Object? balance = const $CopyWithPlaceholder(),
+    Object? lastChecked = const $CopyWithPlaceholder(),
+  }) {
+    return TransactionsAndBalanceState(
+      transactions:
+          transactions == const $CopyWithPlaceholder() || transactions == null
+              ? _value.transactions
+              // ignore: cast_nullable_to_non_nullable
+              : transactions as List<Transaction>,
+      balance: balance == const $CopyWithPlaceholder() || balance == null
+          ? _value.balance
+          // ignore: cast_nullable_to_non_nullable
+          : balance as int,
+      lastChecked:
+          lastChecked == const $CopyWithPlaceholder() || lastChecked == null
+              ? _value.lastChecked
+              // ignore: cast_nullable_to_non_nullable
+              : lastChecked as DateTime,
+    );
+  }
+}
+
+extension $TransactionsAndBalanceStateCopyWith on TransactionsAndBalanceState {
+  /// Returns a callable class that can be used as follows: `instanceOfTransactionsAndBalanceState.copyWith(...)` or like so:`instanceOfTransactionsAndBalanceState.copyWith.fieldName(...)`.
+  // ignore: library_private_types_in_public_api
+  _$TransactionsAndBalanceStateCWProxy get copyWith =>
+      _$TransactionsAndBalanceStateCWProxyImpl(this);
+}
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+Transaction _$TransactionFromJson(Map<String, dynamic> json) => Transaction(
+      from: json['from'] as String,
+      to: json['to'] as String,
+      amount: json['amount'] as int,
+      comment: json['comment'] as String,
+      time: DateTime.parse(json['time'] as String),
+      toAvatar: uIntFromList(json['toAvatar'] as List<int>),
+      toNick: json['toNick'] as String?,
+      fromAvatar: uIntFromList(json['fromAvatar'] as List<int>),
+      fromNick: json['fromNick'] as String?,
+    );
+
+Map<String, dynamic> _$TransactionToJson(Transaction instance) =>
+    <String, dynamic>{
+      'from': instance.from,
+      'to': instance.to,
+      'toAvatar': uIntToList(instance.toAvatar),
+      'toNick': instance.toNick,
+      'amount': instance.amount,
+      'fromAvatar': uIntToList(instance.fromAvatar),
+      'fromNick': instance.fromNick,
+      'comment': instance.comment,
+      'time': instance.time.toIso8601String(),
+    };
+
+TransactionsAndBalanceState _$TransactionsAndBalanceStateFromJson(
+        Map<String, dynamic> json) =>
+    TransactionsAndBalanceState(
+      transactions: (json['transactions'] as List<dynamic>)
+          .map((e) => Transaction.fromJson(e as Map<String, dynamic>))
+          .toList(),
+      balance: json['balance'] as int,
+      lastChecked: DateTime.parse(json['lastChecked'] as String),
+    );
+
+Map<String, dynamic> _$TransactionsAndBalanceStateToJson(
+        TransactionsAndBalanceState instance) =>
+    <String, dynamic>{
+      'transactions': instance.transactions,
+      'balance': instance.balance,
+      'lastChecked': instance.lastChecked.toIso8601String(),
+    };
diff --git a/lib/data/models/transaction_cubit.dart b/lib/data/models/transaction_cubit.dart
new file mode 100644
index 00000000..05e2df97
--- /dev/null
+++ b/lib/data/models/transaction_cubit.dart
@@ -0,0 +1,68 @@
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import '../../../g1/api.dart';
+import '../../../g1/transaction_parser.dart';
+import '../../main.dart';
+import '../../shared_prefs.dart';
+import 'node_list_cubit.dart';
+import 'transaction.dart';
+
+class TransactionsCubit extends HydratedCubit<TransactionsAndBalanceState> {
+  TransactionsCubit()
+      : super(TransactionsAndBalanceState(
+            transactions: const <Transaction>[],
+            balance: 0,
+            lastChecked: DateTime.now()));
+
+  void addTransaction(Transaction transaction) {
+    final TransactionsAndBalanceState currentState = state;
+    final List<Transaction> newTransactions =
+        List<Transaction>.of(currentState.transactions)..add(transaction);
+    final int newBalance = currentState.balance + transaction.amount;
+    emit(currentState.copyWith(
+        transactions: newTransactions, balance: newBalance));
+  }
+
+  void updateTransactions(List<Transaction> newTransactions, int newBalance) {
+    emit(state.copyWith(transactions: newTransactions, balance: newBalance));
+  }
+
+  Future<void> fetchTransactions(NodeListCubit cubit) async {
+    // Future<TransactionsAndBalance> _loadTransactions(NodeListCubit cubit) async {
+    // carga de datos asíncrona
+    // ...
+    // disabled, as we have to change the nodes
+    // https://g1.asycn.io/gva
+    // https://duniter.pini.fr/gva
+    /* Gva(node: 'https://g1.asycn.io/gva')
+        .balance(SharedPreferencesHelper().getPubKey())
+        .then((double currentBal) => setState(() {
+              _balanceAmount = currentBal;
+            })); */
+    logger('Loading transactions');
+    const bool debugging = true;
+    final String txData = debugging
+        ? await getTxHistory(
+            cubit, '6DrGg8cftpkgffv4Y4Lse9HSjgc8coEQor3yvMPHAnVH')
+        : await getTxHistory(cubit, SharedPreferencesHelper().getPubKey());
+    final TransactionsAndBalanceState state = transactionParser(txData);
+    emit(state.copyWith(
+        transactions: state.transactions,
+        balance: state.balance,
+        lastChecked: state.lastChecked));
+  }
+
+  @override
+  TransactionsAndBalanceState fromJson(Map<String, dynamic> json) =>
+      TransactionsAndBalanceState.fromJson(json);
+
+  @override
+  Map<String, dynamic> toJson(TransactionsAndBalanceState state) =>
+      state.toJson();
+
+  List<Transaction> get transactions => state.transactions;
+
+  int get balance => state.balance;
+
+  DateTime get lastChecked => state.lastChecked;
+}
diff --git a/lib/g1/api.dart b/lib/g1/api.dart
index c3216f4b..0175df33 100644
--- a/lib/g1/api.dart
+++ b/lib/g1/api.dart
@@ -1,12 +1,15 @@
 import 'dart:convert';
+// import 'dart:developer' as developer;
 import 'dart:io';
 
 import 'package:flutter/foundation.dart';
 import 'package:http/http.dart' as http;
 import 'package:http/http.dart';
 
+import '../data/models/contact.dart';
 import '../data/models/node.dart';
 import '../data/models/node_list_cubit.dart';
+import '../data/models/node_list_state.dart';
 import '../main.dart';
 import 'g1_helper.dart';
 
@@ -37,16 +40,14 @@ Future<Response> getPeers(NodeListCubit nodeListCubit) async {
 
 Future<Response> searchUser(
     NodeListCubit nodeListCubit, String searchTerm) async {
-  final Response response = await requestWithRetry(
-      nodeListCubit, '/wot/lookup/$searchTerm',
+  final Response response = await requestCPlusWithRetry(nodeListCubit,
+      '/user/profile/_search?q=title:*$searchTerm* OR _id:$searchTerm* OR _id:$searchTerm',
       retryWith404: false);
   return response;
 }
 
 /*
-
 http://doc.e-is.pro/cesium-plus-pod/REST_API.html#userprofile
-
 Not found sample:
 {
 "_index": "user",
@@ -55,12 +56,27 @@ Not found sample:
 "found": false
 }
  */
+Future<Contact> getWot(NodeListCubit nodeListCubit, Contact contact) async {
+  final Response response = await requestCPlusWithRetry(
+      nodeListCubit, '/wot/lookup/${contact.pubkey}');
+  if (response.statusCode == HttpStatus.ok) {
+    final Map<String, dynamic> data =
+        json.decode(response.body) as Map<String, dynamic>;
+    final Map<String, dynamic> results =
+        data['results'] as Map<String, dynamic>;
+    final List<dynamic> uids = results['uids'] as List<dynamic>;
+    if (uids.isNotEmpty) {
+      return contact.copyWith(nick: uids[0]!['uid'] as String);
+    }
+  }
+  return contact;
+}
 
-Future<String> getDataImageFromKey(
+@Deprecated('use getProfile')
+Future<String> _getDataImageFromKey(
     NodeListCubit nodeListCubit, String publicKey) async {
-  // FIXME (vjrj) use node manager and retry...
-  final String url = 'https://g1.data.le-sou.org/user/profile/$publicKey';
-  final Response response = await http.get(Uri.parse(url));
+  final Response response =
+      await requestCPlusWithRetry(nodeListCubit, '/user/profile/$publicKey');
   if (response.statusCode == HttpStatus.ok) {
     final Map<String, dynamic> data =
         json.decode(response.body) as Map<String, dynamic>;
@@ -82,27 +98,44 @@ Uint8List imageFromBase64String(String base64String) {
       base64Decode(base64String.substring(base64String.indexOf(',') + 1)));
 }
 
-Future<Uint8List> getAvatar(NodeListCubit nodeListCubit, String pubKey) async {
-  final String dataImage = await getDataImageFromKey(nodeListCubit, pubKey);
+Future<Uint8List> _getAvata2r(
+    NodeListCubit nodeListCubit, String pubKey) async {
+  final String dataImage = await _getDataImageFromKey(nodeListCubit, pubKey);
   return imageFromBase64String(dataImage);
 }
 
-Future<void> fetchDuniterNodes(NodeListCubit cubit) async {
+Future<void> fetchDuniterNodes(NodeListState state, NodeListCubit cubit,
+    {bool force = false}) async {
   const int minutesToWait = 45;
-  if (DateTime.now()
-          .difference(cubit.lastFetchNodesTime)
-          .compareTo(const Duration(minutes: minutesToWait)) >
-      0) {
-    logger(
-        'Fetching nodes as we did it more than ${minutesToWait}min ago: ${cubit.lastFetchNodesTime.toIso8601String()}');
+  if (force ||
+      /* DateTime.now()
+              .difference(state.lastFetchNodesTime)
+              .compareTo(const Duration(minutes: minutesToWait)) >
+          0 || */
+      duniterNodesWorking(state) < NodeListCubit.maxNodes) {
+    if (force) {
+      cubit.setDuniterNodes(defaultDuniterNodes);
+      logger('Fetching nodes forced');
+    } else {
+      logger(
+          'Fetching nodes as we did it more than ${minutesToWait}min ago: ${state.lastFetchNodesTime.toIso8601String()} and we have only ${duniterNodesWorking(state)}');
+    }
     final List<Node> nodes = await fetchNodesFromApi(cubit);
     cubit.setDuniterNodes(nodes);
   } else {
     logger(
-        'Skipping to fetch nodes as we already did it less than ${minutesToWait}min ago');
+        'Skipping to fetch nodes as we already did it less than ${minutesToWait}min ago and we have ${duniterNodesWorking(state)}');
+    if (!kReleaseMode) {
+      // developer.log(StackTrace.current.toString());
+    }
   }
 }
 
+int duniterNodesWorking(NodeListState state) => state.duniterNodes
+    .where((n) => n.errors < NodeListCubit.maxNodeErrors)
+    .toList()
+    .length;
+
 Future<List<Node>> fetchNodesFromApi(NodeListCubit cubit) async {
   final List<Node> lNodes = <Node>[];
   // To compare with something...
@@ -121,6 +154,8 @@ Future<List<Node>> fetchNodesFromApi(NodeListCubit cubit) async {
           .where((dynamic peer) =>
               (peer as Map<String, dynamic>)['status'] == 'UP')
           .toList();
+      // reorder peer list
+      peers.shuffle();
       for (final dynamic peerR in peers) {
         final Map<String, dynamic> peer = peerR as Map<String, dynamic>;
         if (peer['endpoints'] != null) {
@@ -131,29 +166,43 @@ Future<List<Node>> fetchNodesFromApi(NodeListCubit cubit) async {
               final String endpointUnParsed = endpoints[j];
               final String? endpoint = parseHost(endpointUnParsed);
               if (endpoint != null) {
-                final Duration latency = await _pingNode(endpoint);
-                if (fastestNode == null || latency < fastestLatency) {
-                  fastestNode = endpoint;
-                  fastestLatency = latency;
-                  if (!kReleaseMode) {
-                    logger('Node bloc: Current faster node $fastestNode');
+                try {
+                  final Duration latency = await _pingNode(endpoint);
+                  logger(
+                      'Evaluating node: $endpoint, latency ${latency.inMicroseconds}');
+                  final Node node =
+                      Node(url: endpoint, latency: latency.inMicroseconds);
+                  if (fastestNode == null || latency < fastestLatency) {
+                    fastestNode = endpoint;
+                    fastestLatency = latency;
+                    if (!kReleaseMode) {
+                      logger('Node bloc: Current faster node $fastestNode');
+                    }
+                    cubit.insertDuniterNode(node);
+                    lNodes.insert(0, node);
+                  } else {
+                    // Not the faster
+                    cubit.addDuniterNode(node);
+                    lNodes.add(node);
                   }
+                } catch (e) {
+                  logger('Error fetching $endpoint, error: $e');
                 }
-                final Node node =
-                    Node(url: endpoint, latency: latency.inMicroseconds);
-                cubit.insertDuniterNode(node);
-                lNodes.insert(0, node);
               }
             }
           }
+          if (lNodes.length >= NodeListCubit.maxNodes) {
+            logger('We have enought nodes for now');
+            break;
+          }
         }
       }
     }
     logger(
         'Fetched ${lNodes.length} duniter nodes ordered by latency (first: ${lNodes.first.url})');
   } catch (e) {
-    logger('Error: $e');
-    rethrow;
+    logger('General error in fetch nodes: $e');
+    // rethrow;
   }
   lNodes.sort((Node a, Node b) => a.latency.compareTo(b.latency));
   logger('First node in list ${lNodes.first.url}');
@@ -163,7 +212,10 @@ Future<List<Node>> fetchNodesFromApi(NodeListCubit cubit) async {
 Future<Duration> _pingNode(String node) async {
   try {
     final Stopwatch stopwatch = Stopwatch()..start();
-    await http.get(Uri.parse('$node/network/peers/self/ping'));
+    await http
+        .get(Uri.parse('$node/network/peers/self/ping'))
+        // Decrease http timeout during ping
+        .timeout(const Duration(seconds: 10));
     stopwatch.stop();
     return stopwatch.elapsed;
   } catch (e) {
@@ -189,15 +241,16 @@ Future<http.Response> _requestWithRetry(NodeListCubit cubit, List<Node> nodes,
     String path, bool dontRecord, bool retryWith404) async {
   for (int i = 0; i < nodes.length; i++) {
     final Node node = nodes[i];
-    if (node.errors >= 3) {
+    if (node.errors >= NodeListCubit.maxNodeErrors) {
       // Too much errors skip
       continue;
     }
-    final Uri url = Uri.parse('${node.url}$path');
-    logger('Trying $url');
     try {
+      final Uri url = Uri.parse('${node.url}$path');
+      logger('Fetching $url');
       final int startTime = DateTime.now().millisecondsSinceEpoch;
-      final Response response = await http.get(url);
+      final Response response =
+          await http.get(url).timeout(const Duration(seconds: 10));
       final int endTime = DateTime.now().millisecondsSinceEpoch;
       final int newLatency = endTime - startTime;
       if (response.statusCode == 200) {
@@ -208,13 +261,20 @@ Future<http.Response> _requestWithRetry(NodeListCubit cubit, List<Node> nodes,
       } else if (response.statusCode == 404) {
         logger('404 on fetch $url');
         if (retryWith404) {
+          // Retry with other nodes
+          cubit.updateDuniterNode(node.copyWith(errors: node.errors + 1));
           continue;
         } else {
           return response;
         }
+      } else {
+        logger('${response.statusCode} error on $url');
+        cubit.updateDuniterNode(node.copyWith(errors: node.errors + 1));
       }
     } catch (e) {
+      logger('Error trying ${node.url} $e');
       if (!dontRecord) {
+        logger('Increasing node errors of ${node.url} (${node.errors})');
         cubit.updateDuniterNode(node.copyWith(errors: node.errors + 1));
       }
       continue;
diff --git a/lib/g1/g1_helper.dart b/lib/g1/g1_helper.dart
index 5c9ee3ed..e926b33d 100644
--- a/lib/g1/g1_helper.dart
+++ b/lib/g1/g1_helper.dart
@@ -4,6 +4,7 @@ import 'dart:typed_data';
 
 import 'package:durt/durt.dart';
 
+import '../data/models/payment_state.dart';
 import '../main.dart';
 
 Random createRandom() {
@@ -54,9 +55,12 @@ String? parseHost(String endpointUnParsed) {
         RegExp(r'^\/[a-zA-Z0-9\-\/]+$').hasMatch(lastPart) ? lastPart : '';
 
     final String nextToLast = parts[parts.length - 1];
-    final String port = lastPart == ''
-        ? (RegExp(r'^\/[0-9]$').hasMatch(lastPart) ? lastPart : '443')
-        : RegExp(r'^\/[0-9]$').hasMatch(nextToLast)
+    /* print(lastPart);
+    print(path);
+    print(nextToLast); */
+    final String port = path == ''
+        ? (RegExp(r'^[0-9]+$').hasMatch(lastPart) ? lastPart : '443')
+        : RegExp(r'^[0-9]+$').hasMatch(nextToLast)
             ? nextToLast
             : '443';
     final List<String> hostSplited = parts[1].split('/');
@@ -84,3 +88,48 @@ String? parseHost(String endpointUnParsed) {
     return null;
   }
 }
+
+bool validateKey(String pubKey) {
+  return RegExp(
+          r'^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{44}$')
+      .hasMatch(pubKey);
+}
+
+String getQrUri(String destinationPublicKey, [String amountString = '0']) {
+  final double amount = double.tryParse(amountString) ?? 0.0;
+
+  String uri;
+  if (amount > 0) {
+    // there is something like this in other clients?
+    uri = 'duniter:key/$destinationPublicKey?amount=$amount';
+  } else {
+    uri = destinationPublicKey;
+  }
+  return uri;
+}
+
+PaymentState? parseScannedUri(String qr) {
+  final RegExp regexKeyAmount = RegExp(r'duniter:key/(\w+)\?amount=([\d.]+)');
+  final RegExpMatch? matchKeyAmount = regexKeyAmount.firstMatch(qr);
+
+  if (matchKeyAmount != null) {
+    final String publicKey = matchKeyAmount.group(1)!;
+    final double amount = double.parse(matchKeyAmount.group(2)!);
+    return PaymentState(publicKey: publicKey, amount: amount);
+  }
+
+  // Match no amount
+  final RegExp regexKey = RegExp(r'duniter:key/(\w+)');
+  final RegExpMatch? matchKey = regexKey.firstMatch(qr);
+  if (matchKey != null) {
+    final String publicKey = matchKey.group(1)!;
+    return PaymentState(publicKey: publicKey);
+  }
+
+  // Match key only
+  if (validateKey(qr)) {
+    return PaymentState(publicKey: qr);
+  }
+
+  return null;
+}
diff --git a/lib/g1/transaction.dart b/lib/g1/transaction.dart
deleted file mode 100644
index 2215c2ae..00000000
--- a/lib/g1/transaction.dart
+++ /dev/null
@@ -1,73 +0,0 @@
-import 'package:hydrated_bloc/hydrated_bloc.dart';
-
-class Transaction {
-  Transaction(
-      {required this.from,
-      required this.to,
-      required this.amount,
-      required this.comment,
-      required this.time});
-
-  factory Transaction.fromMap(Map<String, dynamic> map) {
-    return Transaction(
-      from: map['from'] as String,
-      to: map['to'] as String,
-      amount: map['amount'] as int,
-      comment: map['comment'] as String,
-      time: DateTime.fromMillisecondsSinceEpoch(map['time'] as int),
-    );
-  }
-
-  final String from;
-  final String to;
-  final int amount;
-  final String comment;
-  final DateTime time;
-
-  Map<String, dynamic> toMap() {
-    return <String, dynamic>{
-      'from': from,
-      'to': to,
-      'amount': amount,
-      'comment': comment,
-      'time': time.millisecondsSinceEpoch,
-    };
-  }
-}
-
-class TransactionListBloc
-    extends HydratedBloc<List<Transaction>, List<Transaction>> {
-  TransactionListBloc() : super(<Transaction>[]);
-
-  void addTransaction(Transaction transaction) {
-    add(state..add(transaction));
-  }
-
-  void removeTransaction(Transaction transaction) {
-    add(state..remove(transaction));
-  }
-
-  @override
-  List<Transaction> fromJson(Map<String, dynamic> json) {
-    final List<Map<String, dynamic>> transactions =
-        List<Map<String, dynamic>>.from(
-            json['transactions'] as List<Map<String, dynamic>>);
-    return transactions
-        .map((dynamic e) => Transaction.fromMap(e as Map<String, dynamic>))
-        .toList();
-  }
-
-  @override
-  Map<String, dynamic> toJson(List<Transaction> state) {
-    final List<dynamic> transactions =
-        state.map((dynamic e) => e.toMap()).toList();
-    return <String, dynamic>{'transactions': transactions};
-  }
-}
-
-class TransactionsAndBalance {
-  TransactionsAndBalance({required this.transactions, required this.balance});
-
-  List<Transaction> transactions;
-  int balance;
-}
diff --git a/lib/g1/transaction_parser.dart b/lib/g1/transaction_parser.dart
index 7e9a853c..dd021a8c 100644
--- a/lib/g1/transaction_parser.dart
+++ b/lib/g1/transaction_parser.dart
@@ -1,8 +1,8 @@
 import 'dart:convert';
 
-import 'transaction.dart';
+import '../data/models/transaction.dart';
 
-TransactionsAndBalance transactionParser(String txData) {
+TransactionsAndBalanceState transactionParser(String txData) {
   final RegExp exp = RegExp(r'\((.*?)\)');
 
   final Map<String, dynamic> parsedTxData =
@@ -14,7 +14,7 @@ TransactionsAndBalance transactionParser(String txData) {
   final List<Transaction> tx = <Transaction>[];
   for (final dynamic receivedRaw in listReceived) {
     final Map<String, dynamic> received = receivedRaw as Map<String, dynamic>;
-    final int time = received['blockstampTime'] as int;
+    final int timestamp = received['blockstampTime'] as int;
     final String comment = received['comment'] as String;
     final List<dynamic> outputs = received['outputs'] as List<dynamic>;
     final int amount = int.parse((outputs[0] as String).split(':')[0]);
@@ -28,12 +28,21 @@ TransactionsAndBalance transactionParser(String txData) {
       // Send
       balance = balance -= amount;
     }
-    tx.add(Transaction(
-        from: address2!,
-        to: address1!,
-        amount: pubKey == address2 ? -amount : amount,
-        comment: comment,
-        time: DateTime.fromMillisecondsSinceEpoch(time)));
+    final DateTime txDate =
+        DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
+    /* if (!kReleaseMode) {
+      logger('Timestamp: $timestamp');
+      logger('Fecha: $txDate');
+    } */
+    tx.insert(
+        0,
+        Transaction(
+            from: address2!,
+            to: address1!,
+            amount: pubKey == address2 ? -amount : amount,
+            comment: comment,
+            time: txDate));
   }
-  return TransactionsAndBalance(transactions: tx, balance: balance);
+  return TransactionsAndBalanceState(
+      transactions: tx, balance: balance, lastChecked: DateTime.now());
 }
diff --git a/lib/main.dart b/lib/main.dart
index 461c3f96..beac35e8 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -14,13 +14,19 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
 import 'package:introduction_screen/introduction_screen.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:responsive_framework/responsive_wrapper.dart';
+import 'package:responsive_framework/utils/scroll_behavior.dart';
+import 'package:sentry_flutter/sentry_flutter.dart';
 
 import 'app_bloc_observer.dart';
 import 'config/theme.dart';
+import 'cubit/bottom_nav_cubit.dart';
 import 'data/models/app_cubit.dart';
 import 'data/models/app_state.dart';
+import 'data/models/contact_cubit.dart';
 import 'data/models/node_list_cubit.dart';
+import 'data/models/node_list_state.dart';
 import 'data/models/payment_cubit.dart';
+import 'data/models/transaction_cubit.dart';
 import 'g1/api.dart';
 import 'shared_prefs.dart';
 import 'ui/screens/skeleton_screen.dart';
@@ -71,7 +77,7 @@ void main() async {
     final Directory tmpDir = await getTemporaryDirectory();
     Hive.init(tmpDir.toString());
     HydratedBloc.storage =
-        await HydratedStorage.build(storageDirectory: tmpDir);
+    await HydratedStorage.build(storageDirectory: tmpDir);
   }
 
   // Reset hive during developing
@@ -79,26 +85,45 @@ void main() async {
     // await HydratedBloc.storage.clear();
   }
 
-  runApp(
-    EasyLocalization(
-      path: 'assets/translations',
-      supportedLocales: const <Locale>[
-        Locale('en'),
-        Locale('es'),
-        Locale('fr'),
-      ],
-      fallbackLocale: const Locale('en'),
-      useFallbackTranslations: true,
-      child: MultiBlocProvider(providers: <BlocProvider<dynamic>>[
-        BlocProvider<AppCubit>(create: (BuildContext context) => AppCubit()),
-        BlocProvider<PaymentCubit>(
-            create: (BuildContext context) => PaymentCubit()),
-        BlocProvider<NodeListCubit>(
-            create: (BuildContext context) => NodeListCubit()),
-        // Add other BlocProviders here if needed
-      ], child: const MyApp()),
-    ),
-  );
+  void appRunner() =>
+      runApp(
+        EasyLocalization(
+          path: 'assets/translations',
+          supportedLocales: const <Locale>[
+            Locale('en'),
+            Locale('es'),
+            Locale('fr'),
+          ],
+          fallbackLocale: const Locale('en'),
+          useFallbackTranslations: true,
+          child: MultiBlocProvider(providers: <BlocProvider<dynamic>>[
+            BlocProvider<BottomNavCubit>(
+                create: (BuildContext context) => BottomNavCubit()),
+            BlocProvider<AppCubit>(
+                create: (BuildContext context) => AppCubit()),
+            BlocProvider<PaymentCubit>(
+                create: (BuildContext context) => PaymentCubit()),
+            BlocProvider<NodeListCubit>(
+                create: (BuildContext context) => NodeListCubit()),
+            BlocProvider<ContactsCubit>(
+                create: (BuildContext context) => ContactsCubit()),
+            BlocProvider<TransactionsCubit>(
+                create: (BuildContext context) => TransactionsCubit())
+            // Add other BlocProviders here if needed
+          ], child: const GinkgoApp()),
+        ),
+      );
+
+  if (!kReleaseMode) {
+    await SentryFlutter.init((SentryFlutterOptions options,) {
+      options.dsn = "${dotenv.env['SENTRY_DSN']}";
+      // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
+      // We recommend adjusting this value in production.
+      // options.tracesSampleRate = 1.0;
+    }, appRunner: appRunner);
+  } else {
+    appRunner();
+  }
 }
 
 class AppIntro extends StatefulWidget {
@@ -110,10 +135,10 @@ class AppIntro extends StatefulWidget {
 
 class _AppIntro extends State<AppIntro> {
   final GlobalKey<IntroductionScreenState> introKey =
-      GlobalKey<IntroductionScreenState>();
+  GlobalKey<IntroductionScreenState>();
 
   void _onIntroEnd(BuildContext context) {
-    BlocProvider.of<AppCubit>(context).introViewed();
+    context.read<AppCubit>().introViewed();
     Navigator.of(context).pushReplacement(
       MaterialPageRoute<void>(
           builder: (BuildContext _) => const SkeletonScreen()),
@@ -155,8 +180,8 @@ class _AppIntro extends State<AppIntro> {
   }
 }
 
-PageViewModel createPageViewModel(
-    String title, String body, String imageAsset) {
+PageViewModel createPageViewModel(String title, String body,
+    String imageAsset) {
   return PageViewModel(
     title: tr(title),
     body: tr(body),
@@ -169,75 +194,84 @@ PageViewModel createPageViewModel(
   );
 }
 
-class MyApp extends StatefulWidget {
-  const MyApp({super.key});
+class GinkgoApp extends StatefulWidget {
+  const GinkgoApp({super.key});
 
   @override
-  State<MyApp> createState() => _MyAppState();
+  State<GinkgoApp> createState() => _GinkgoAppState();
 }
 
-class _MyAppState extends State<MyApp> {
+class _GinkgoAppState extends State<GinkgoApp> {
   @override
   Widget build(BuildContext context) {
     return ConnectivityAppWrapper(
         app: MaterialApp(
-      /// Localization is not available for the title.
-      title: 'Äž1nkgo',
-      theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
-      darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
-
-      /// Theme stuff
-
-      /// Localization stuff
-      localizationsDelegates: context.localizationDelegates,
-      supportedLocales: context.supportedLocales,
-      locale: context.locale,
-      debugShowCheckedModeBanner: false,
-      home: MediaQuery(
-        data: const MediaQueryData(),
-        child: BlocProvider.of<AppCubit>(context).isIntroViewed
-            ? const SkeletonScreen()
-            : const AppIntro(),
-      ),
-      builder: (BuildContext buildContext, Widget? widget) {
-        final NodeListCubit nodeListCubit =
-            BlocProvider.of<NodeListCubit>(buildContext);
-        final int nDuniterNodes = nodeListCubit.duniterNodes.length;
-        final int nCesiumPlusNodes = nodeListCubit.cesiumPlusNodes.length;
-
-        // Load nodes from /network/peers
-        fetchDuniterNodes(nodeListCubit);
-
-        logger(
-            'Starting with $nDuniterNodes duniter nodes and $nCesiumPlusNodes c+ nodes');
-
-        final Cron cron = Cron();
-        cron.schedule(Schedule.parse('*/45 * * * *'), () async {
-          // Every 45m check for faster node (maybe it something costly in terms of
-          // bandwidth
-          fetchDuniterNodes(nodeListCubit);
-        });
-        cron.schedule(Schedule.parse('*/90 * * * *'), () async {
-          nodeListCubit.cleanDuniterErrorStats();
-        });
-
-        return ResponsiveWrapper.builder(
-          ConnectivityWidgetWrapper(
-            message: tr('offline'),
-            height: 20,
-            child: widget!,
-          ),
-          maxWidth: 480,
-          minWidth: 480,
-          // defaultScale: true,
-          breakpoints: <ResponsiveBreakpoint>[
-            // const ResponsiveBreakpoint.resize(200, name: MOBILE),
-            const ResponsiveBreakpoint.resize(480, name: TABLET),
-            const ResponsiveBreakpoint.resize(480, name: DESKTOP),
-          ],
-          background: Container(color: const Color(0xFFF5F5F5)),
-        );
-      },
-    ));
+
+          /// Localization is not available for the title.
+            title: 'Äž1nkgo',
+            theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
+            darkTheme:
+            ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
+
+            /// Theme stuff
+
+            /// Localization stuff
+            localizationsDelegates: context.localizationDelegates,
+            supportedLocales: context.supportedLocales,
+            locale: context.locale,
+            debugShowCheckedModeBanner: false,
+            home: context
+                .read<AppCubit>()
+                .isIntroViewed
+                ? const SkeletonScreen()
+                : const AppIntro(),
+            builder: (BuildContext buildContext, Widget? widget) {
+              return BlocBuilder<NodeListCubit, NodeListState>(
+                builder: (BuildContext nodeListContext,
+                    NodeListState nodeListState) {
+                  final int nDuniterNodes = nodeListState.duniterNodes.length;
+                  final int nCesiumPlusNodes =
+                      nodeListState.cesiumPlusNodes.length;
+
+                  final NodeListCubit nodeListCubit =
+                  nodeListContext.read<NodeListCubit>();
+                  // Load nodes from /network/peers
+                  fetchDuniterNodes(nodeListState, nodeListCubit);
+
+                  logger(
+                      'Starting with $nDuniterNodes duniter nodes and $nCesiumPlusNodes c+ nodes');
+
+
+                  final Cron cron = Cron();
+                  cron.schedule(Schedule.parse('*/45 * * * *'), () async {
+                    // Every 45m check for faster node (maybe it something costly in terms of
+                    // bandwidth
+                    fetchDuniterNodes(nodeListState, nodeListCubit);
+                  });
+                  cron.schedule(Schedule.parse('*/90 * * * *'), () async {
+                    nodeListCubit.cleanDuniterErrorStats();
+                  });
+
+                  return ResponsiveWrapper.builder(
+                    BouncingScrollWrapper.builder(
+                        context,
+                        ConnectivityWidgetWrapper(
+                          message: tr('offline'),
+                          height: 20,
+                          child: widget!,
+                        )),
+                    maxWidth: 480,
+                    minWidth: 480,
+                    // defaultScale: true,
+                    breakpoints: <ResponsiveBreakpoint>[
+                      // const ResponsiveBreakpoint.resize(200, name: MOBILE),
+                      const ResponsiveBreakpoint.resize(480, name: TABLET),
+                      const ResponsiveBreakpoint.resize(480, name: DESKTOP),
+                    ],
+                    background: Container(color: const Color(0xFFF5F5F5)),
+                  );
+                },
+              );
+            }));
   }
 }
diff --git a/lib/ui/screens/fifth_screen.dart b/lib/ui/screens/fifth_screen.dart
index 60af0a29..90f5d29b 100644
--- a/lib/ui/screens/fifth_screen.dart
+++ b/lib/ui/screens/fifth_screen.dart
@@ -1,10 +1,14 @@
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 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 '../../g1/api.dart';
 import '../../g1/export_import.dart';
 import '../ui_helpers.dart';
+import '../widgets/bottom_widget.dart';
 import '../widgets/fifth_screen/grid_item.dart';
 import '../widgets/fifth_screen/info_card.dart';
 import '../widgets/fifth_screen/link_card.dart';
@@ -18,6 +22,8 @@ class FifthScreen extends StatelessWidget {
   Widget build(BuildContext context) {
     return BlocBuilder<NodeListCubit, NodeListState>(
         builder: (BuildContext context, NodeListState state) {
+      final NodeListCubit nodeListCubit = context.read<NodeListCubit>();
+      final List<Node> duniterNodes = nodeListCubit.duniterNodes;
       return Material(
         color: Theme.of(context).colorScheme.background,
         child: ListView(
@@ -25,15 +31,18 @@ class FifthScreen extends StatelessWidget {
             physics: const BouncingScrollPhysics(),
             children: <Widget>[
               const Header(text: 'bottom_nav_fifth'),
-              InfoCard(
-                  title: 'connected-to',
-                  subtitle: context
-                      .read<NodeListCubit>()
-                      .duniterNodes
-                      .first
-                      .url
-                      .replaceFirst(':443', ''),
-                  icon: Icons.hub),
+              GestureDetector(
+                  onLongPress: () {
+                    fetchDuniterNodes(state, nodeListCubit, 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,
@@ -49,7 +58,7 @@ class FifthScreen extends StatelessWidget {
                   padding: EdgeInsets.zero,
                   children: <GridItem>[
                     GridItem(
-                        title: 'export-key',
+                        title: 'export_key',
                         icon: Icons.download,
                         onTap: () {
                           showDialog(
@@ -60,18 +69,18 @@ class FifthScreen extends StatelessWidget {
                           );
                         }),
                     GridItem(
-                        title: 'import-key',
+                        title: 'import_key',
                         icon: Icons.upload,
                         onTap: () {
                           const ExportImportPage();
                         }),
                     GridItem(
-                      title: 'copy-your-key',
+                      title: 'copy_your_key',
                       icon: Icons.copy,
                       onTap: () => copyPublicKeyToClipboard(context),
                     )
                   ]),
-              const SizedBox(height: 36),
+              const BottomWidget()
             ]),
       );
     });
diff --git a/lib/ui/screens/first_screen.dart b/lib/ui/screens/first_screen.dart
index a229d9d9..5b963b49 100644
--- a/lib/ui/screens/first_screen.dart
+++ b/lib/ui/screens/first_screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import '../../data/models/app_cubit.dart';
-import '../../data/models/app_state.dart';
+import '../widgets/bottom_widget.dart';
 import '../widgets/first_screen/credit_card.dart';
 import '../widgets/first_screen/pay_contact_search_widget.dart';
 import '../widgets/header.dart';
@@ -17,53 +17,51 @@ class FirstScreen extends StatefulWidget {
 }
 
 class _FirstScreenState extends State<FirstScreen> {
-  final ScrollController _controller = ScrollController();
+  // final ScrollController _controller = ScrollController();
 
   @override
   Widget build(BuildContext context) {
-    return BlocBuilder<AppCubit, AppState>(
-        builder: (BuildContext context, AppState state) {
-      WidgetsBinding.instance.addPostFrameCallback((_) async {
-        if (!BlocProvider.of<AppCubit>(context).isWarningViewed) {
-          ScaffoldMessenger.of(context).showSnackBar(
-            SnackBar(
-              content: Text(tr('demo-desc')),
-              action: SnackBarAction(
-                label: 'OK',
-                onPressed: () {
-                  ScaffoldMessenger.of(context).hideCurrentSnackBar();
-                  BlocProvider.of<AppCubit>(context).warningViewed();
-                },
-              ),
+    WidgetsBinding.instance.addPostFrameCallback((_) async {
+      if (!context.read<AppCubit>().isWarningViewed) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(
+            content: Text(tr('demo_desc')),
+            action: SnackBarAction(
+              label: 'OK',
+              onPressed: () {
+                ScaffoldMessenger.of(context).hideCurrentSnackBar();
+                context.read<AppCubit>().warningViewed();
+              },
             ),
-          );
-        }
-      });
-      return Material(
-          color: Theme.of(context).colorScheme.background,
-          child: ListView(
-              padding: const EdgeInsets.symmetric(horizontal: 16),
-              physics: const AlwaysScrollableScrollPhysics(),
-              controller: _controller,
-              shrinkWrap: true,
-              children: <Widget>[
-                const Header(text: 'credit_card_title'),
-                CreditCard(),
-                const SizedBox(height: 8),
-                Padding(
-                  padding: const EdgeInsets.symmetric(horizontal: 24),
-                  child: Divider(
-                    color: Theme.of(context)
-                        .colorScheme
-                        .onBackground
-                        .withOpacity(.4),
-                  ),
-                ),
-                const SizedBox(height: 10),
-                const PayContactSearchWidget(),
-                const SizedBox(height: 10),
-                const PayForm()
-              ]));
+          ),
+        );
+      }
     });
+    return Material(
+        color: Theme.of(context).colorScheme.background,
+        child: ListView(
+            padding: const EdgeInsets.symmetric(horizontal: 16),
+            //physics: const AlwaysScrollableScrollPhysics(),
+            //controller: _controller,
+            // shrinkWrap: true,
+            children: <Widget>[
+              const Header(text: 'credit_card_title'),
+              CreditCard(),
+              const SizedBox(height: 8),
+              Padding(
+                padding: const EdgeInsets.symmetric(horizontal: 24),
+                child: Divider(
+                  color: Theme.of(context)
+                      .colorScheme
+                      .onBackground
+                      .withOpacity(.4),
+                ),
+              ),
+              const SizedBox(height: 10),
+              const PayContactSearchWidget(),
+              const SizedBox(height: 10),
+              const PayForm(),
+              const BottomWidget()
+            ]));
   }
 }
diff --git a/lib/ui/screens/skeleton_screen.dart b/lib/ui/screens/skeleton_screen.dart
index c078a0c7..8b55b9fb 100644
--- a/lib/ui/screens/skeleton_screen.dart
+++ b/lib/ui/screens/skeleton_screen.dart
@@ -23,25 +23,23 @@ class SkeletonScreen extends StatelessWidget {
       FifthScreen(),
     ];
 
-    return BlocProvider<BottomNavCubit>(
-        create: (BuildContext context) => BottomNavCubit(),
-        child: Scaffold(
-          extendBodyBehindAppBar: true,
-          resizeToAvoidBottomInset: true,
-          appBar: const AppBarGone(),
+    return Scaffold(
+      extendBodyBehindAppBar: true,
+      resizeToAvoidBottomInset: true,
+      appBar: const AppBarGone(),
 
-          /// When switching between tabs this will fade the old
-          /// layout out and the new layout in.
-          body: BlocBuilder<BottomNavCubit, int>(
-            builder: (BuildContext context, int state) {
-              return AnimatedSwitcher(
-                  duration: const Duration(milliseconds: 300),
-                  child: pageNavigation.elementAt(state));
-            },
-          ),
+      /// When switching between tabs this will fade the old
+      /// layout out and the new layout in.
+      body: BlocBuilder<BottomNavCubit, int>(
+        builder: (BuildContext context, int state) {
+          return AnimatedSwitcher(
+              duration: const Duration(milliseconds: 300),
+              child: pageNavigation.elementAt(state));
+        },
+      ),
 
-          bottomNavigationBar: const BottomNavBar(),
-          backgroundColor: Theme.of(context).colorScheme.background,
-        ));
+      bottomNavigationBar: const BottomNavBar(),
+      backgroundColor: Theme.of(context).colorScheme.background,
+    );
   }
 }
diff --git a/lib/ui/screens/third_screen.dart b/lib/ui/screens/third_screen.dart
index 81b1455f..ff3d5816 100644
--- a/lib/ui/screens/third_screen.dart
+++ b/lib/ui/screens/third_screen.dart
@@ -1,5 +1,7 @@
 import 'package:flutter/material.dart';
 
+import '../widgets/third_screen/contacts_page.dart';
+
 class ThirdScreen extends StatelessWidget {
   const ThirdScreen({super.key});
 
@@ -7,10 +9,7 @@ class ThirdScreen extends StatelessWidget {
   Widget build(BuildContext context) {
     return Material(
       color: Theme.of(context).colorScheme.background,
-      child: ListView(
-          padding: const EdgeInsets.symmetric(horizontal: 16),
-          physics: const BouncingScrollPhysics(),
-          children: const <Widget>[SizedBox(height: 36), Text('Contacts')]),
+      child: const ContactsPage(),
     );
   }
 }
diff --git a/lib/ui/ui_helpers.dart b/lib/ui/ui_helpers.dart
index 503ef55e..dcdfe8b0 100644
--- a/lib/ui/ui_helpers.dart
+++ b/lib/ui/ui_helpers.dart
@@ -3,6 +3,7 @@ import 'dart:typed_data';
 import 'package:clipboard/clipboard.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:timeago/timeago.dart' as timeago;
 
 import '../shared_prefs.dart';
 import 'widgets/first_screen/circular_icon.dart';
@@ -30,7 +31,7 @@ void showTooltip(BuildContext context, String title, String message) {
 void copyPublicKeyToClipboard(BuildContext context) {
   FlutterClipboard.copy(SharedPreferencesHelper().getPubKey()).then(
       (dynamic value) => ScaffoldMessenger.of(context).showSnackBar(
-          const SnackBar(content: Text('key-copied-to-clipboard'))));
+          SnackBar(content: Text(tr('key_copied_to_clipboard')))));
 }
 
 const Color defAvatarBgColor = Colors.grey;
@@ -58,4 +59,17 @@ String humanizeFromToPubKey(String publicAddress, String address) {
   }
 }
 
-String humanizePubKey(String address) => '\u{1F511}${address.substring(0, 8)}';
+String humanizePubKey(String address) => '\u{1F511} ${address.substring(0, 8)}';
+
+Widget humanizePubKeyAsWidget(String pubKey) => Text(
+      humanizePubKey(pubKey),
+      style: const TextStyle(
+        fontSize: 16.0,
+      ),
+    );
+
+Color tileColor(int index, [bool inverse = false]) =>
+    (inverse ? index.isOdd : index.isEven) ? Colors.grey[200]! : Colors.white;
+
+String? humanizeTime(DateTime time, String locale) =>
+    timeago.format(time, locale: locale, clock: DateTime.now());
diff --git a/lib/ui/widgets/bottom_widget.dart b/lib/ui/widgets/bottom_widget.dart
new file mode 100644
index 00000000..312db2ed
--- /dev/null
+++ b/lib/ui/widgets/bottom_widget.dart
@@ -0,0 +1,10 @@
+import 'package:flutter/material.dart';
+
+class BottomWidget extends StatelessWidget {
+  const BottomWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const SizedBox(height: 36);
+  }
+}
diff --git a/lib/ui/widgets/fifth_screen/info_card.dart b/lib/ui/widgets/fifth_screen/info_card.dart
index 50c5c50f..2add7f4f 100644
--- a/lib/ui/widgets/fifth_screen/info_card.dart
+++ b/lib/ui/widgets/fifth_screen/info_card.dart
@@ -7,11 +7,13 @@ class InfoCard extends StatelessWidget {
       {super.key,
       required this.title,
       this.subtitle = '',
+      this.trailing = '',
       required this.icon,
       this.translate = true});
 
   final String title;
   final String subtitle;
+  final String trailing;
   final IconData icon;
   final bool translate;
 
@@ -35,6 +37,7 @@ class InfoCard extends StatelessWidget {
             shape: const RoundedRectangleBorder(
                 borderRadius: BorderRadius.all(Radius.circular(12))),
             subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
+            trailing: trailing.isNotEmpty ? Text(trailing) : null,
             title: Row(
               children: <Widget>[
                 Icon(icon, color: Theme.of(context).colorScheme.primary),
diff --git a/lib/ui/widgets/first_screen/credit_card.dart b/lib/ui/widgets/first_screen/credit_card.dart
index c01e9855..91f87fb8 100644
--- a/lib/ui/widgets/first_screen/credit_card.dart
+++ b/lib/ui/widgets/first_screen/credit_card.dart
@@ -1,5 +1,6 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
 import 'package:flutter_svg/svg.dart';
 import 'package:qr_flutter/qr_flutter.dart';
 
@@ -53,12 +54,12 @@ class CreditCard extends StatelessWidget {
                     spreadRadius: 1.0,
                   )
                 ],
-                gradient: const LinearGradient(
+                gradient: LinearGradient(
                   begin: Alignment.bottomLeft,
                   end: Alignment.topRight,
                   colors: <Color>[
-                    Color(0xFF05112B),
-                    Color(0xFF085476),
+                    Color(int.parse("${dotenv.env['CARD_COLOR_LEFT']}")),
+                    Color(int.parse("${dotenv.env['CARD_COLOR_RIGHT']}")),
                   ],
                 ),
               ),
@@ -73,7 +74,7 @@ class CreditCard extends StatelessWidget {
                   Padding(
                     padding: const EdgeInsets.all(cardPadding),
                     child: Text(
-                      tr('g1_wallet'),
+                      dotenv.env['CARD_COLOR_TEXT'] ?? tr('g1_wallet'),
                       style: const TextStyle(
                         color: Colors.white,
                         fontSize: 24.0,
@@ -99,7 +100,7 @@ class CreditCard extends StatelessWidget {
                       child: Row(children: <Widget>[
                         GestureDetector(
                             onTap: () =>
-                                showTooltip(context, '', tr('keys-tooltip')),
+                                showTooltip(context, '', tr('keys_tooltip')),
                             child: Text('**** **** ', style: cardTextStyle)),
                         GestureDetector(
                             onTap: () => copyPublicKeyToClipboard(context),
@@ -114,9 +115,9 @@ class CreditCard extends StatelessWidget {
                         const EdgeInsets.symmetric(horizontal: cardPadding),
                     child: GestureDetector(
                       onTap: () =>
-                          showTooltip(context, '', tr('card-validity-tooltip')),
+                          showTooltip(context, '', tr('card_validity_tooltip')),
                       child: Text(
-                        tr('card-validity'),
+                        tr('card_validity'),
                         style: TextStyle(
                           decoration: TextDecoration.underline,
                           color: Colors.white.withOpacity(0.8),
@@ -135,18 +136,22 @@ class CreditCard extends StatelessWidget {
       context: context,
       builder: (BuildContext context) {
         return Dialog(
-            child: Container(
-          padding: const EdgeInsets.all(16.0),
-          child: GestureDetector(
-            onTap: () => copyPublicKeyToClipboard(context),
-            child: QrImage(
-              data: publicKey,
-              size: MediaQuery.of(context).size.width * 0.8,
-              gapless: false,
-              foregroundColor: Colors.orange,
-            ),
-          ),
-        ));
+            child: SizedBox(
+                height: MediaQuery.of(context).size.width,
+                child: Padding(
+                    padding: const EdgeInsets.all(16.0),
+                    child: GestureDetector(
+                      onTap: () => copyPublicKeyToClipboard(context),
+                      child: Column(children: <Widget>[
+                        Text(tr('show_qr_to_client')),
+                        QrImage(
+                          data: publicKey,
+                          size: MediaQuery.of(context).size.width * 0.8,
+                          gapless: false,
+                          foregroundColor: Colors.orange,
+                        ),
+                      ]),
+                    ))));
       },
     );
   }
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 755b502f..16c032d7 100644
--- a/lib/ui/widgets/first_screen/pay_contact_search_dialog.dart
+++ b/lib/ui/widgets/first_screen/pay_contact_search_dialog.dart
@@ -5,14 +5,22 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:http/http.dart';
+import 'package:simple_barcode_scanner/enum.dart';
 import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
 
+import '../../../cubit/bottom_nav_cubit.dart';
+import '../../../data/models/contact.dart';
+import '../../../data/models/contact_cubit.dart';
+import '../../../data/models/contact_state.dart';
 import '../../../data/models/node_list_cubit.dart';
-import '../../../data/models/node_list_state.dart';
 import '../../../data/models/payment_cubit.dart';
+import '../../../data/models/payment_state.dart';
 import '../../../g1/api.dart';
+import '../../../g1/g1_helper.dart';
+import '../../../main.dart';
 import '../../ui_helpers.dart';
 import '../loading_box.dart';
+import '../third_screen/contacts_page.dart';
 
 class SearchDialog extends StatefulWidget {
   const SearchDialog({super.key});
@@ -25,7 +33,7 @@ class _SearchDialogState extends State<SearchDialog> {
   final TextEditingController _searchController = TextEditingController();
   String _searchTerm = '';
 
-  List<dynamic> _results = <dynamic>[];
+  List<Contact> _results = <Contact>[];
   bool _isLoading = false;
 
   Future<void> _search(NodeListCubit cubit) async {
@@ -35,18 +43,23 @@ class _SearchDialogState extends State<SearchDialog> {
 
     final Response response = await searchUser(cubit, _searchTerm);
     if (response.statusCode == 404) {
+      _results = <Contact>[];
+      _isLoading = false;
+      if (validateKey(_searchTerm)) {
+        // looks like a plain key
+        final Contact contact = Contact(pubkey: _searchTerm);
+        _results.add(contact);
+      }
       setState(() {
-        _results = <dynamic>[];
         _isLoading = false;
-        // FIXME (vjrj): give some feedback;
       });
     } else {
+      _results = (((const JsonDecoder().convert(response.body)
+      as Map<String, dynamic>)['hits']
+      as Map<String, dynamic>)['hits'] as List<dynamic>)
+          .map((e) => _contactFromResult(e as Map<String, dynamic>))
+          .toList();
       setState(() {
-        _results = (const JsonDecoder().convert(response.body)
-            as Map<String, dynamic>)['results'] as List<dynamic>;
-        // debugPrint(_results.toString());
-        // FIXME (vjrj) search in the blockchain and if it's a key, just
-        // put the key as a result
         _isLoading = false;
       });
     }
@@ -54,129 +67,188 @@ class _SearchDialogState extends State<SearchDialog> {
 
   @override
   Widget build(BuildContext context) {
-    bool isFavorite = false;
-    return BlocBuilder<NodeListCubit, NodeListState>(
-        builder: (BuildContext context, NodeListState state) {
-      final NodeListCubit nodeListCubit = context.read<NodeListCubit>();
-      return Scaffold(
-        appBar: AppBar(
-          title: Text(tr('search_user_title')),
-          backgroundColor: Theme.of(context).colorScheme.primary,
-          foregroundColor: Colors.white,
-          actions: <Widget>[
-            IconButton(
-                icon: const Icon(Icons.qr_code_scanner),
-                onPressed: () async {
-                  final String? publicKey = await Navigator.push(
-                      context,
-                      MaterialPageRoute<String>(
-                        builder: (BuildContext context) =>
-                            const SimpleBarcodeScannerPage(),
-                      ));
-                  setState(() {
-                    if (publicKey is String) {
-                      _searchController.text = publicKey;
-                      _search(nodeListCubit);
-                    }
-                  });
-                }),
-            IconButton(
-              icon: const Icon(Icons.close),
-              onPressed: () => Navigator.pop(context),
-            )
-          ],
-        ),
-        body: Padding(
-          padding: const EdgeInsets.all(16.0),
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.stretch,
-            children: <Widget>[
-              TextField(
-                controller: _searchController,
-                decoration: InputDecoration(
-                  filled: true,
-                  fillColor: Colors.white,
-                  labelText: tr('search_user'),
-                  suffixIcon: IconButton(
-                    icon: const Icon(Icons.search),
-                    onPressed: () {
-                      _search(nodeListCubit);
-                    },
-                  ),
+    final NodeListCubit nodeListCubit = context.read<NodeListCubit>();
+    final PaymentCubit paymentCubit = context.read<PaymentCubit>();
+    final BottomNavCubit nav = context.read<BottomNavCubit>();
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(tr('search_user_title')),
+        backgroundColor: Theme
+            .of(context)
+            .colorScheme
+            .primary,
+        foregroundColor: Colors.white,
+        actions: <Widget>[
+          IconButton(
+              icon: const Icon(Icons.qr_code_scanner),
+              onPressed: () async {
+                final String? scannedKey = await Navigator.push(
+                    context,
+                    MaterialPageRoute<String>(
+                      builder: (BuildContext context) =>
+                          SimpleBarcodeScannerPage(
+                              scanType: ScanType.qr,
+                              appBarTitle: tr('qr-scanner-title'),
+                              cancelButtonText: tr('close')),
+                    ));
+                if (scannedKey is String &&
+                    scannedKey != null &&
+                    scannedKey != '-1') {
+                  PaymentState? pay = parseScannedUri(scannedKey);
+                  await _search(nodeListCubit);
+                  if (_results.length == 1 && pay != null) {
+                    final Contact contact = _results[0];
+                    pay = pay.copyWith(
+                        nick: contact.name, avatar: contact.avatar);
+                  }
+                  if (pay!.amount != null) {
+                    paymentCubit.selectKeyAmount(pay.publicKey, pay.amount!);
+                  } else {
+                    paymentCubit.selectKey(pay.publicKey);
+                  }
+                  nav.updateIndex(0);
+                }
+              }),
+          IconButton(
+            icon: const Icon(Icons.close),
+            onPressed: () => Navigator.pop(context),
+          )
+        ],
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(16.0),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: <Widget>[
+            TextField(
+              controller: _searchController,
+              decoration: InputDecoration(
+                filled: true,
+                fillColor: Colors.white,
+                labelText: tr('search_user'),
+                suffixIcon: IconButton(
+                  icon: const Icon(Icons.search),
+                  onPressed: () {
+                    _search(nodeListCubit);
+                  },
                 ),
-                onChanged: (String value) {
-                  setState(() {
-                    _searchTerm = value;
-                  });
-                },
-                onSubmitted: (_) {
-                  _search(nodeListCubit);
-                },
               ),
-              if (_isLoading)
-                const LoadingBox()
+              onChanged: (String value) {
+                setState(() {
+                  _searchTerm = value;
+                });
+              },
+              onSubmitted: (_) {
+                _search(nodeListCubit);
+              },
+            ),
+            if (_isLoading)
+              const LoadingBox()
+            else
+              if (_searchTerm.isNotEmpty && _results.isEmpty && _isLoading)
+                const NoElements(text: 'nothing_found')
               else
                 Expanded(
                   child: ListView.builder(
                       itemCount: _results.length,
                       itemBuilder: (BuildContext context, int index) {
-                        final String uid =
-                            (((_results[index] as Map<String, dynamic>)['uids']
-                                    as List<dynamic>)[0]
-                                as Map<String, dynamic>)['uid'] as String;
-                        final String pubkey = (_results[index]
-                            as Map<String, dynamic>)['pubkey'] as String;
-                        return FutureBuilder<Uint8List>(
-                            future: getAvatar(
-                                nodeListCubit,
-                                (_results[index]
-                                        as Map<String, dynamic>)['pubkey']
-                                    as String),
+                        final Contact contact = _results[index];
+                        // FIMXE final String nick = _getNick(currentIndex);
+                        final String nick = contact.name ?? contact.pubkey;
+                        final String pubKey = contact.pubkey;
+                        return FutureBuilder<Contact>(
+                            future: getWot(nodeListCubit, contact),
                             builder: (BuildContext context,
-                                AsyncSnapshot<Uint8List> snapshot) {
-                              return ListTile(
-                                title: Text(uid),
-                                tileColor: index.isEven
-                                    ? Colors.grey[200]
-                                    : Colors.white,
-                                onTap: () {
-                                  context
-                                      .read<PaymentCubit>()
-                                      .selectUser(pubkey, uid, snapshot.data!);
-                                  Navigator.pop(context, _results[index]);
-                                },
-                                leading: avatar(
-                                  snapshot.hasData,
-                                  snapshot.data,
-                                  bgColor: index.isEven
-                                      ? defAvatarColor
-                                      : defAvatarBgColor,
-                                  color: index.isEven
-                                      ? defAvatarBgColor
-                                      : defAvatarColor,
-                                ),
-                                trailing: IconButton(
-                                  icon: Icon(
-                                    isFavorite
-                                        ? Icons.favorite
-                                        : Icons.favorite_border,
-                                    color:
-                                        isFavorite ? Colors.red.shade400 : null,
-                                  ),
-                                  onPressed: () {
-                                    setState(() {
-                                      isFavorite = !isFavorite;
-                                    });
+                                AsyncSnapshot<Contact> snapshot) {
+                              Widget widget;
+                              if (snapshot.hasData) {
+                                final bool hasAvatar = snapshot.hasData &&
+                                    snapshot.data!.avatar != null;
+                                logger('Contact retrieved ${snapshot.data}');
+                                widget = ListTile(
+                                  title: Text(nick),
+                                  tileColor: tileColor(index),
+                                  onTap: () {
+                                    context.read<PaymentCubit>().selectUser(
+                                        pubKey,
+                                        nick,
+                                        hasAvatar
+                                            ? snapshot.data!.avatar
+                                            : null);
+                                    Navigator.pop(context);
                                   },
-                                ),
-                              );
+                                  leading: avatar(
+                                    hasAvatar,
+                                    hasAvatar ? snapshot.data!.avatar : null,
+                                    bgColor: tileColor(index, true),
+                                    color: tileColor(index),
+                                  ),
+                                  trailing:
+                                  BlocBuilder<ContactsCubit, ContactsState>(
+                                      builder: (BuildContext context,
+                                          ContactsState state) {
+                                        final ContactsCubit contactsCubit =
+                                        context.read<ContactsCubit>();
+                                        final bool isFavorite =
+                                        contactsCubit.isContact(pubKey);
+                                        return IconButton(
+                                          icon: Icon(
+                                            isFavorite
+                                                ? Icons.favorite
+                                                : Icons.favorite_border,
+                                            color: isFavorite
+                                                ? Colors.red.shade400
+                                                : null,
+                                          ),
+                                          onPressed: () {
+                                            if (snapshot.hasData) {
+                                              setState(() {
+                                                if (!isFavorite) {
+                                                  contactsCubit
+                                                      .addContact(
+                                                      snapshot.data!);
+                                                } else {
+                                                  contactsCubit.removeContact(
+                                                      Contact(
+                                                        pubkey: pubKey,
+                                                      ));
+                                                }
+                                              });
+                                            }
+                                          },
+                                        );
+                                      }),
+                                );
+                              } else if (snapshot.hasError) {
+                                widget = Padding(
+                                  padding: const EdgeInsets.only(top: 16),
+                                  child: Text('Error: ${snapshot.error}'),
+                                );
+                              } else {
+                                widget = ListTile(
+                                  tileColor: tileColor(index),
+                                );
+                              }
+                              return widget;
                             });
                       }),
                 )
-            ],
-          ),
+          ],
         ),
-      );
-    });
+      ),
+    );
+  }
+
+  Contact _contactFromResult(Map<String, dynamic> record) {
+    final Map<String, dynamic> source =
+    record['_source'] as Map<String, dynamic>;
+    final Map<String, dynamic> avatar =
+    source['avatar'] as Map<String, dynamic>;
+    final Uint8List avatarBase64 = imageFromBase64String(
+        'data:${avatar['_content_type']};base64,${avatar['_content']}');
+    return Contact(
+        pubkey: record['_id'] as String,
+        name: source['title'] as String,
+        avatar: avatarBase64);
   }
 }
diff --git a/lib/ui/widgets/first_screen/recipient_widget.dart b/lib/ui/widgets/first_screen/recipient_widget.dart
index be706da5..60d32ee2 100644
--- a/lib/ui/widgets/first_screen/recipient_widget.dart
+++ b/lib/ui/widgets/first_screen/recipient_widget.dart
@@ -24,18 +24,13 @@ class RecipientWidget extends StatelessWidget {
                         crossAxisAlignment: CrossAxisAlignment.start,
                         children: <Widget>[
                           Text(
-                            state.nick,
+                            state.nick ?? '',
                             style: const TextStyle(
                               fontSize: 20.0,
                               fontWeight: FontWeight.bold,
                             ),
                           ),
-                          Text(
-                            humanizePubKey(state.publicKey),
-                            style: const TextStyle(
-                              fontSize: 16.0,
-                            ),
-                          ),
+                          humanizePubKeyAsWidget(state.publicKey),
                         ],
                       ),
                     ),
diff --git a/lib/ui/widgets/fourth_screen/balance_chart.dart b/lib/ui/widgets/fourth_screen/balance_chart.dart
index d1d3d561..957eaa8e 100644
--- a/lib/ui/widgets/fourth_screen/balance_chart.dart
+++ b/lib/ui/widgets/fourth_screen/balance_chart.dart
@@ -1,7 +1,7 @@
 import 'package:fl_chart/fl_chart.dart';
 import 'package:flutter/material.dart';
 
-import '../../../g1/transaction.dart';
+import '../../../data/models/transaction.dart';
 
 class BalanceChart extends StatefulWidget {
   const BalanceChart({super.key, required this.transactions});
diff --git a/lib/ui/widgets/fourth_screen/transaction_chart.dart b/lib/ui/widgets/fourth_screen/transaction_chart.dart
new file mode 100644
index 00000000..0cbbbdc1
--- /dev/null
+++ b/lib/ui/widgets/fourth_screen/transaction_chart.dart
@@ -0,0 +1,366 @@
+import 'package:fl_chart/fl_chart.dart';
+import 'package:flutter/material.dart';
+
+class TransactionChart extends StatefulWidget {
+  TransactionChart({super.key});
+
+  final Color leftBarColor = Colors.yellow;
+  final Color rightBarColor = Colors.red;
+  final Color avgColor = Colors.orange;
+
+  @override
+  State<StatefulWidget> createState() => TransactionChartState();
+}
+
+class TransactionChartState extends State<TransactionChart> {
+  final double width = 7;
+
+  late List<BarChartGroupData> rawBarGroups;
+  late List<BarChartGroupData> showingBarGroups;
+
+  int touchedGroupIndex = -1;
+
+  String _selectedButton = 'DAY';
+  final List<String> _buttonValues = ['DAY', 'WEEK', 'MONTH', 'YEAR'];
+
+  List<BarChartGroupData> _getChartData(String buttonValue) {
+    return <BarChartGroupData>[];
+  }
+
+  Widget _buildButton(String buttonValue) {
+    final isSelected = _selectedButton == buttonValue;
+    return ElevatedButton(
+      onPressed: () {
+        setState(() {
+          _selectedButton = buttonValue;
+          showingBarGroups = _getChartData(buttonValue);
+        });
+      },
+      child: Text(buttonValue),
+      style: ButtonStyle(
+        backgroundColor: MaterialStateProperty.all(
+          isSelected ? Colors.blue : Colors.grey,
+        ),
+      ),
+    );
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    final BarChartGroupData barGroup1 = makeGroupData(0, 5, 12);
+    final BarChartGroupData barGroup2 = makeGroupData(1, 16, 12);
+    final BarChartGroupData barGroup3 = makeGroupData(2, 18, 5);
+    final BarChartGroupData barGroup4 = makeGroupData(3, 20, 16);
+    final BarChartGroupData barGroup5 = makeGroupData(4, 17, 6);
+    final BarChartGroupData barGroup6 = makeGroupData(5, 19, 1.5);
+    final BarChartGroupData barGroup7 = makeGroupData(6, 10, 1.5);
+
+    final List<BarChartGroupData> items = <BarChartGroupData>[
+      barGroup1,
+      barGroup2,
+      barGroup3,
+      barGroup4,
+      barGroup5,
+      barGroup6,
+      barGroup7,
+    ];
+
+    rawBarGroups = items;
+
+    showingBarGroups = rawBarGroups;
+  }
+
+  Widget build(BuildContext context) {
+    return AspectRatio(
+      aspectRatio: 1,
+      child: Padding(
+        padding: const EdgeInsets.all(16),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: <Widget>[
+            Expanded(
+              child: BarChart(
+                BarChartData(
+                  maxY: 20,
+                  barTouchData: BarTouchData(
+                    touchTooltipData: BarTouchTooltipData(
+                      tooltipBgColor: Colors.grey,
+                      getTooltipItem: (BarChartGroupData a, int b,
+                              BarChartRodData c, int d) =>
+                          null,
+                    ),
+                    touchCallback:
+                        (FlTouchEvent event, BarTouchResponse? response) {
+                      if (response == null || response.spot == null) {
+                        setState(() {
+                          touchedGroupIndex = -1;
+                          showingBarGroups =
+                              List<BarChartGroupData>.of(rawBarGroups);
+                        });
+                        return;
+                      }
+
+                      touchedGroupIndex = response.spot!.touchedBarGroupIndex;
+
+                      setState(() {
+                        if (!event.isInterestedForInteractions) {
+                          touchedGroupIndex = -1;
+                          showingBarGroups =
+                              List<BarChartGroupData>.of(rawBarGroups);
+                          return;
+                        }
+                        showingBarGroups =
+                            List<BarChartGroupData>.of(rawBarGroups);
+                        if (touchedGroupIndex != -1) {
+                          double sum = 0.0;
+                          for (final BarChartRodData rod
+                              in showingBarGroups[touchedGroupIndex].barRods) {
+                            sum += rod.toY;
+                          }
+                          final double avg = sum /
+                              showingBarGroups[touchedGroupIndex]
+                                  .barRods
+                                  .length;
+
+                          showingBarGroups[touchedGroupIndex] =
+                              showingBarGroups[touchedGroupIndex].copyWith(
+                            barRods: showingBarGroups[touchedGroupIndex]
+                                .barRods
+                                .map((BarChartRodData rod) {
+                              return rod.copyWith(
+                                  toY: avg, color: widget.avgColor);
+                            }).toList(),
+                          );
+                        }
+                      });
+                    },
+                  ),
+                  titlesData: FlTitlesData(
+                    show: true,
+                    rightTitles: AxisTitles(
+                      sideTitles: SideTitles(showTitles: false),
+                    ),
+                    topTitles: AxisTitles(
+                      sideTitles: SideTitles(showTitles: false),
+                    ),
+                    bottomTitles: AxisTitles(
+                      sideTitles: SideTitles(
+                        showTitles: true,
+                        getTitlesWidget: bottomTitles,
+                        reservedSize: 42,
+                      ),
+                    ),
+                    leftTitles: AxisTitles(
+                      sideTitles: SideTitles(
+                        showTitles: true,
+                        reservedSize: 28,
+                        interval: 1,
+                        getTitlesWidget: leftTitles,
+                      ),
+                    ),
+                  ),
+                  borderData: FlBorderData(
+                    show: false,
+                  ),
+                  barGroups: showingBarGroups,
+                  gridData: FlGridData(show: false),
+                ),
+              ),
+            ),
+            const SizedBox(
+              height: 12,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget leftTitles(double value, TitleMeta meta) {
+    const TextStyle style = TextStyle(
+      color: Color(0xff7589a2),
+      fontWeight: FontWeight.bold,
+      fontSize: 14,
+    );
+    String text;
+    if (value == 0) {
+      text = '1K';
+    } else if (value == 10) {
+      text = '5K';
+    } else if (value == 19) {
+      text = '10K';
+    } else {
+      return Container();
+    }
+    return SideTitleWidget(
+      axisSide: meta.axisSide,
+      space: 0,
+      child: Text(text, style: style),
+    );
+  }
+
+  Widget bottomTitles(double value, TitleMeta meta) {
+    final List<String> titles = <String>[
+      'Mn',
+      'Te',
+      'Wd',
+      'Tu',
+      'Fr',
+      'St',
+      'Su'
+    ];
+
+    final Widget text = Text(
+      titles[value.toInt()],
+      style: const TextStyle(
+        color: Color(0xff7589a2),
+        fontWeight: FontWeight.bold,
+        fontSize: 14,
+      ),
+    );
+
+    return SideTitleWidget(
+      axisSide: meta.axisSide,
+      space: 16, //margin top
+      child: text,
+    );
+  }
+
+  BarChartGroupData makeGroupData(int x, double y1, double y2) {
+    return BarChartGroupData(
+      barsSpace: 4,
+      x: x,
+      barRods: <BarChartRodData>[
+        BarChartRodData(
+          toY: y1,
+          color: widget.leftBarColor,
+          width: width,
+        ),
+        BarChartRodData(
+          toY: y2,
+          color: widget.rightBarColor,
+          width: width,
+        ),
+      ],
+    );
+  }
+
+  @override
+  Widget buildButtons(BuildContext context) {
+    return AspectRatio(
+        aspectRatio: 1,
+        child: Padding(
+            padding: const EdgeInsets.all(16),
+            child: Column(
+                crossAxisAlignment: CrossAxisAlignment.stretch,
+                children: <Widget>[
+                  const SizedBox(
+                    height: 38,
+                  ),
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      ElevatedButton(
+                        onPressed: () {
+                          setState(() {
+                            // Cambiar la lógica aquí para actualizar el gráfico para mostrar datos diarios
+                          });
+                        },
+                        child: Text('Dia'),
+                      ),
+                      const SizedBox(width: 16),
+                      ElevatedButton(
+                        onPressed: () {
+                          setState(() {
+                            // Cambiar la lógica aquí para actualizar el gráfico para mostrar datos semanales
+                          });
+                        },
+                        child: Text('Semana'),
+                      ),
+                      const SizedBox(width: 16),
+                      ElevatedButton(
+                        onPressed: () {
+                          setState(() {
+                            // Cambiar la lógica aquí para actualizar el gráfico para mostrar datos mensuales
+                          });
+                        },
+                        child: Text('Mes'),
+                      ),
+                      const SizedBox(width: 16),
+                      ElevatedButton(
+                        onPressed: () {
+                          setState(() {
+                            // Cambiar la lógica aquí para actualizar el gráfico para mostrar datos anuales
+                          });
+                        },
+                        child: Text('Año'),
+                      ),
+                    ],
+                  ),
+                  Expanded(
+                    child: Column(
+                      children: <Widget>[
+                        Row(
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          children: [
+                            _buildButton('Day'),
+                            _buildButton('Week'),
+                            _buildButton('Month'),
+                            _buildButton('Year'),
+                          ],
+                        ),
+                        const SizedBox(height: 16),
+                        BarChart(
+                          BarChartData(
+                            maxY: 20,
+                            barTouchData: BarTouchData(
+                              touchTooltipData: BarTouchTooltipData(
+                                tooltipBgColor: Colors.grey,
+                                getTooltipItem: (a, b, c, d) => null,
+                              ),
+                              touchCallback: (FlTouchEvent event, response) {
+                                // ...
+                              },
+                            ),
+                            titlesData: FlTitlesData(
+                              show: true,
+                              rightTitles: AxisTitles(
+                                sideTitles: SideTitles(showTitles: false),
+                              ),
+                              topTitles: AxisTitles(
+                                sideTitles: SideTitles(showTitles: false),
+                              ),
+                              bottomTitles: AxisTitles(
+                                sideTitles: SideTitles(
+                                  showTitles: true,
+                                  getTitlesWidget: bottomTitles,
+                                  reservedSize: 42,
+                                ),
+                              ),
+                              leftTitles: AxisTitles(
+                                sideTitles: SideTitles(
+                                  showTitles: true,
+                                  reservedSize: 28,
+                                  interval: 1,
+                                  getTitlesWidget: leftTitles,
+                                ),
+                              ),
+                            ),
+                            borderData: FlBorderData(
+                              show: false,
+                            ),
+                            barGroups: showingBarGroups,
+                            gridData: FlGridData(show: false),
+                          ),
+                        ),
+                        const SizedBox(height: 12),
+                      ],
+                    ),
+                  ),
+                  const SizedBox(
+                    height: 12,
+                  )
+                ])));
+  }
+}
diff --git a/lib/ui/widgets/fourth_screen/transaction_page.dart b/lib/ui/widgets/fourth_screen/transaction_page.dart
index dab72ef6..9c99bdf4 100644
--- a/lib/ui/widgets/fourth_screen/transaction_page.dart
+++ b/lib/ui/widgets/fourth_screen/transaction_page.dart
@@ -1,17 +1,19 @@
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
 
+import '../../../data/models/contact.dart';
+import '../../../data/models/contact_cubit.dart';
 import '../../../data/models/node_list_cubit.dart';
-import '../../../data/models/node_list_state.dart';
-import '../../../g1/api.dart';
-import '../../../g1/transaction.dart';
-import '../../../g1/transaction_parser.dart';
+import '../../../data/models/transaction.dart';
+import '../../../data/models/transaction_cubit.dart';
 import '../../../shared_prefs.dart';
 import '../../ui_helpers.dart';
 import '../header.dart';
 import '../loading_box.dart';
+import '../third_screen/contacts_page.dart';
+import 'transaction_chart.dart';
 
 class TransactionsAndBalanceWidget extends StatefulWidget {
   const TransactionsAndBalanceWidget({super.key});
@@ -24,150 +26,207 @@ class TransactionsAndBalanceWidget extends StatefulWidget {
 class _TransactionsAndBalanceWidgetState
     extends State<TransactionsAndBalanceWidget>
     with SingleTickerProviderStateMixin {
-  late ScrollController _transactionListController;
-
-  /* late double _balanceAmount;
-  List<Transaction> _transactions = <Transaction>[];
-  bool _isLoading = true; */
+  final ScrollController _transScrollController = ScrollController();
+  late NodeListCubit nodeListCubit;
+  late TransactionsCubit transCubit;
+  bool isLoading = false;
+  bool debugging = false;
 
   @override
   void initState() {
     super.initState();
-    _transactionListController = ScrollController();
+    _transScrollController.addListener(_scrollListener);
+    transCubit = context.read<TransactionsCubit>();
+    nodeListCubit = context.read<NodeListCubit>();
+    transCubit.fetchTransactions(nodeListCubit);
   }
 
   @override
   void dispose() {
-    _transactionListController.dispose();
+    _transScrollController.removeListener(_scrollListener);
+    _transScrollController.dispose();
     super.dispose();
   }
 
-  Future<TransactionsAndBalance> _loadTransactions(NodeListCubit cubit) async {
-    // carga de datos asíncrona
-    // ...
-    // disabled, as we have to change the nodes
-    // https://g1.asycn.io/gva
-    // https://duniter.pini.fr/gva
-    /* Gva(node: 'https://g1.asycn.io/gva')
-        .balance(SharedPreferencesHelper().getPubKey())
-        .then((double currentBal) => setState(() {
-              _balanceAmount = currentBal;
-            })); */
-
-    final String txData = !kReleaseMode
-        ? await getTxHistory(
-            cubit, '6DrGg8cftpkgffv4Y4Lse9HSjgc8coEQor3yvMPHAnVH')
-        : await getTxHistory(cubit, SharedPreferencesHelper().getPubKey());
-    final TransactionsAndBalance result = transactionParser(txData);
-    /*  .then((String txData) {
-    final TransactionsAndBalance result = transactionParser(txData);
-    setState(() {
-      _balanceAmount = result.balance / 100;
-      _transactions = result.transactions;
-      _isLoading = false;
-    });
-  });  */
-    return result;
+  Future<void> _scrollListener() async {
+    if (_transScrollController.position.pixels ==
+            _transScrollController.position.maxScrollExtent ||
+        _transScrollController.offset == 0) {
+      setState(() {
+        isLoading = true;
+      });
+      await transCubit.fetchTransactions(nodeListCubit);
+      setState(() {
+        isLoading = false;
+      });
+    }
   }
 
   @override
   Widget build(BuildContext context) {
-    String pubKey = SharedPreferencesHelper().getPubKey();
-    pubKey = '6DrGg8cftpkgffv4Y4Lse9HSjgc8coEQor3yvMPHAnVH';
-    return BlocBuilder<NodeListCubit, NodeListState>(
-        builder: (BuildContext context, NodeListState state) => FutureBuilder<
-                TransactionsAndBalance>(
-            future: _loadTransactions(context.read<NodeListCubit>()),
-            builder: (BuildContext context,
-                AsyncSnapshot<TransactionsAndBalance> results) {
-              if (results.hasData) {
-                return Stack(children: <Widget>[
-                  const Header(text: 'transactions'),
-                  ListView.builder(
-                    shrinkWrap: true,
-                    physics: const BouncingScrollPhysics(),
-                    controller: _transactionListController,
-                    itemCount: results.data!.transactions.length,
-                    itemBuilder: (BuildContext context, int index) {
-                      return ListTile(
-                        title: Text(tr('transaction_from_to',
-                            namedArgs: <String, String>{
-                              'from': humanizeFromToPubKey(pubKey,
-                                  results.data!.transactions[index].from),
-                              'to': humanizeFromToPubKey(
-                                  pubKey, results.data!.transactions[index].to)
-                            })),
-                        subtitle:
-                            results.data!.transactions[index].comment.isNotEmpty
-                                ? Text(
-                                    results.data!.transactions[index].comment,
-                                    style: const TextStyle(
-                                      fontStyle: FontStyle.italic,
+    final String myPubKey = debugging
+        ? SharedPreferencesHelper().getPubKey()
+        : '6DrGg8cftpkgffv4Y4Lse9HSjgc8coEQor3yvMPHAnVH';
+    return BlocBuilder<TransactionsCubit, TransactionsAndBalanceState>(builder:
+        (BuildContext context, TransactionsAndBalanceState transBalanceState) {
+      // Fetch transactions
+      // TODO(vjrj): Only fetch last transactions and used persisted ones
+      final ContactsCubit contactsCubit = context.read<ContactsCubit>();
+      final List<Transaction> transactions = transBalanceState.transactions;
+      final int balance = transBalanceState.balance;
+      if (!isLoading) {
+        return Stack(children: <Widget>[
+          Column(crossAxisAlignment: CrossAxisAlignment.start, children: <
+              Widget>[
+            Container(
+                color: Theme.of(context).colorScheme.surfaceVariant,
+                height: 90,
+                width: double.infinity,
+                child: const Padding(
+                    padding: EdgeInsets.symmetric(horizontal: 16),
+                    child: Header(text: 'transactions'))),
+            Expanded(
+              child: Padding(
+                  padding: const EdgeInsets.fromLTRB(0, 0, 0, 50),
+                  child: transactions.isEmpty
+                      ? Column(children: const <Widget>[
+                          NoElements(text: 'no_transactions')
+                        ])
+                      : ListView.builder(
+                          physics: const AlwaysScrollableScrollPhysics(),
+                          shrinkWrap: true,
+                          controller: _transScrollController,
+                          itemCount: transactions.length,
+                          // Size of elements
+                          // itemExtent: 100,
+                          itemBuilder: (BuildContext context, int index) {
+                            return Slidable(
+                                // Specify a key if the Slidable is dismissible.
+                                key: const ValueKey<int>(0),
+                                // The end action pane is the one at the right or the bottom side.
+                                endActionPane: ActionPane(
+                                  motion: const ScrollMotion(),
+                                  children: <SlidableAction>[
+                                    SlidableAction(
+                                      onPressed: (BuildContext c) {
+                                        _addContact(transactions, index,
+                                            myPubKey, contactsCubit);
+                                        // FIXME i18n
+                                        ScaffoldMessenger.of(context)
+                                            .showSnackBar(
+                                          SnackBar(
+                                            content: Text(tr('contact_added')),
+                                          ),
+                                        );
+                                      },
+                                      backgroundColor:
+                                          Theme.of(context).primaryColor,
+                                      foregroundColor: Colors.white,
+                                      icon: Icons.contacts,
+                                      label: tr('add_contact'),
                                     ),
-                                  )
-                                : null,
-                        tileColor:
-                            index.isEven ? Colors.grey[200] : Colors.white,
-                        trailing: Text(
-                            '${results.data!.transactions[index].amount < 0 ? "" : "+"}${(results.data!.transactions[index].amount / 100).toStringAsFixed(2)} Äž1',
-                            style: TextStyle(
-                                color:
-                                    results.data!.transactions[index].amount < 0
-                                        ? Colors.red
-                                        : Colors.blue)),
-                      );
-                    },
-                  ),
-                  DraggableScrollableSheet(
-                      initialChildSize: 0.1,
-                      minChildSize: 0.1,
-                      maxChildSize: 0.9,
-                      builder: (BuildContext context,
-                              ScrollController scrollController) =>
-                          Container(
-                            decoration: BoxDecoration(
+                                  ],
+                                ),
+                                child: ListTile(
+                                  title: Text(tr('transaction_from_to',
+                                      namedArgs: <String, String>{
+                                        'from': humanizeFromToPubKey(
+                                            myPubKey, transactions[index].from),
+                                        'to': humanizeFromToPubKey(
+                                            myPubKey, transactions[index].to)
+                                      })),
+                                  subtitle: Column(
+                                      crossAxisAlignment:
+                                          CrossAxisAlignment.start,
+                                      children: <Widget>[
+                                        if (transactions[index]
+                                            .comment
+                                            .isNotEmpty)
+                                          Text(
+                                            transactions[index].comment,
+                                            style: const TextStyle(
+                                              fontStyle: FontStyle.italic,
+                                            ),
+                                          ),
+                                        Text(humanizeTime(
+                                            transactions[index].time,
+                                            context.locale.toString())!)
+                                      ]),
+                                  tileColor: tileColor(index),
+                                  trailing: Text(
+                                      '${transactions[index].amount < 0 ? "" : "+"}${(transactions[index].amount / 100).toStringAsFixed(2)} Äž1',
+                                      style: TextStyle(
+                                          color: transactions[index].amount < 0
+                                              ? Colors.red
+                                              : Colors.blue)),
+                                ));
+                          },
+                        )),
+            )
+          ]),
+          Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 10),
+              child: DraggableScrollableSheet(
+                  initialChildSize: 0.12,
+                  minChildSize: 0.12,
+                  maxChildSize: 0.9,
+                  builder: (BuildContext context,
+                          ScrollController scrollController) =>
+                      Container(
+                        decoration: BoxDecoration(
+                          color: Theme.of(context).colorScheme.inversePrimary,
+                          border: Border.all(
                               color:
                                   Theme.of(context).colorScheme.inversePrimary,
-                              border: Border.all(
-                                  color: Theme.of(context)
-                                      .colorScheme
-                                      .inversePrimary,
-                                  width: 3),
-                              borderRadius: const BorderRadius.only(
-                                topLeft: Radius.circular(8),
-                                topRight: Radius.circular(8),
-                              ),
+                              width: 3),
+                          borderRadius: const BorderRadius.only(
+                            topLeft: Radius.circular(8),
+                            topRight: Radius.circular(8),
+                          ),
+                        ),
+                        child: Scrollbar(
+                            child: ListView(
+                          controller: scrollController,
+                          children: <Widget>[
+                            const Padding(
+                                padding: EdgeInsets.symmetric(horizontal: 16),
+                                child: Header(
+                                  text: 'balance',
+                                  topPadding: 0,
+                                )),
+                            Padding(
+                              padding:
+                                  const EdgeInsets.symmetric(vertical: 20.0),
+                              child: Center(
+                                  child: Text(
+                                '${(balance / 100).toStringAsFixed(2)} Äž1',
+                                style: const TextStyle(
+                                    fontSize: 36.0,
+                                    fontWeight: FontWeight.bold),
+                              )),
                             ),
-                            child: Scrollbar(
-                                child: ListView(
-                              controller: scrollController,
-                              children: <Widget>[
-                                const Header(text: 'balance'),
-                                Padding(
-                                  padding: const EdgeInsets.symmetric(
-                                      vertical: 20.0),
-                                  child: Center(
-                                      child: Text(
-                                    '${(results.data!.balance / 100).toStringAsFixed(2)} Äž1',
-                                    style: const TextStyle(
-                                        fontSize: 36.0,
-                                        fontWeight: FontWeight.bold),
-                                  )),
-                                ),
-                                /*
-                          Expanded(
-                              child: BalanceChart(transactions: _transactions)),
-                          */
-                              ],
-                            )),
-                          ))
-                ]);
-              } else if (results.hasError) {
-                // FIXME
-                return const Text('Error al cargar los datos.');
-              } else {
-                return const LoadingBox();
-              }
-            }));
+                            TransactionChart()
+                            /*BalanceChart(
+                                    transactions: .transactions),*/
+                          ],
+                        )),
+                      )))
+        ]);
+      } else {
+        return const LoadingScreen();
+      }
+    });
+  }
+
+  void _addContact(List<Transaction> transactions, int index, String myPubKey,
+      ContactsCubit contactsCubit) {
+    final Transaction tx = transactions[index];
+    final String fromPubKey = tx.from;
+    final String toPubKey = tx.to;
+    final bool useFrom = fromPubKey != myPubKey;
+    contactsCubit.addContact(Contact(
+        pubkey: useFrom ? fromPubKey : toPubKey,
+        nick: useFrom ? tx.fromNick : tx.toNick,
+        avatar: useFrom ? tx.fromAvatar : tx.toAvatar));
   }
 }
diff --git a/lib/ui/widgets/header.dart b/lib/ui/widgets/header.dart
index 0f4bf0c1..cfd37c54 100644
--- a/lib/ui/widgets/header.dart
+++ b/lib/ui/widgets/header.dart
@@ -2,14 +2,15 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 
 class Header extends StatelessWidget {
-  const Header({super.key, required this.text});
+  const Header({super.key, required this.text, this.topPadding = 38});
 
   final String text;
+  final double topPadding;
 
   @override
   Widget build(BuildContext context) {
     return Padding(
-      padding: const EdgeInsets.only(left: 2, right: 2, top: 38, bottom: 14),
+      padding: EdgeInsets.only(left: 2, right: 2, top: topPadding, bottom: 14),
       child: Text(
         tr(text),
         textAlign: TextAlign.start,
diff --git a/lib/ui/widgets/loading_box.dart b/lib/ui/widgets/loading_box.dart
index 37b80185..8e8132c4 100644
--- a/lib/ui/widgets/loading_box.dart
+++ b/lib/ui/widgets/loading_box.dart
@@ -15,3 +15,16 @@ class LoadingBox extends StatelessWidget {
     );
   }
 }
+
+class LoadingScreen extends StatelessWidget {
+  const LoadingScreen({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const Scaffold(
+      body: Center(
+        child: CircularProgressIndicator(),
+      ),
+    );
+  }
+}
diff --git a/lib/ui/widgets/second_screen/card_terminal_screen.dart b/lib/ui/widgets/second_screen/card_terminal_screen.dart
index e37742fd..6afaabdf 100644
--- a/lib/ui/widgets/second_screen/card_terminal_screen.dart
+++ b/lib/ui/widgets/second_screen/card_terminal_screen.dart
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:qr_flutter/qr_flutter.dart';
 
+import '../../../g1/g1_helper.dart';
 import '../../../shared_prefs.dart';
 import 'card_terminal_status.dart';
 
@@ -74,7 +75,7 @@ class CardTerminalScreen extends StatelessWidget {
             Expanded(
                 child: Column(children: <Widget>[
               QrImage(
-                data: _getQrUi(SharedPreferencesHelper().getPubKey(), amount),
+                data: getQrUri(SharedPreferencesHelper().getPubKey(), amount),
                 size: 160.0,
               )
             ])),
@@ -107,8 +108,8 @@ class CardTerminalScreen extends StatelessWidget {
                         decoration: InputDecoration(
                           border: InputBorder.none,
                           hintText: amount.isNotEmpty
-                              ? tr('show-qr-to-client-amount')
-                              : tr('show-qr-to-client'),
+                              ? tr('show_qr_to_client_amount')
+                              : tr('show_qr_to_client'),
                           hintStyle: const TextStyle(
                             fontFamily: 'Roboto Mono',
                             color: Colors.grey,
@@ -126,17 +127,4 @@ class CardTerminalScreen extends StatelessWidget {
       ),
     );
   }
-
-  String _getQrUi(String destinationPublicKey, String amountString) {
-    final double amount = double.tryParse(amountString) ?? 0.0;
-
-    String uri;
-    if (amount > 0) {
-      // there is something like this in other clients?
-      uri = 'duniter:key/$destinationPublicKey?amount=$amount';
-    } else {
-      uri = destinationPublicKey;
-    }
-    return uri;
-  }
 }
diff --git a/lib/ui/widgets/second_screen/card_terminal_status.dart b/lib/ui/widgets/second_screen/card_terminal_status.dart
index 6a5744e4..4bd4068b 100644
--- a/lib/ui/widgets/second_screen/card_terminal_status.dart
+++ b/lib/ui/widgets/second_screen/card_terminal_status.dart
@@ -17,7 +17,7 @@ class CardTerminalStatus extends StatelessWidget {
             color: online ? Colors.green : Colors.red,
           ),
           const SizedBox(width: 8),
-          Text(online ? tr('online-terminal') : tr('offline-terminal'),
+          Text(online ? tr('online_terminal') : tr('offline_terminal'),
               style: const TextStyle(
                 color: Colors.white,
                 fontWeight: FontWeight.bold,
diff --git a/lib/ui/widgets/third_screen/contacts_page.dart b/lib/ui/widgets/third_screen/contacts_page.dart
new file mode 100644
index 00000000..27f2962c
--- /dev/null
+++ b/lib/ui/widgets/third_screen/contacts_page.dart
@@ -0,0 +1,173 @@
+import 'package:clipboard/clipboard.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+
+import '../../../cubit/bottom_nav_cubit.dart';
+import '../../../data/models/contact.dart';
+import '../../../data/models/contact_cubit.dart';
+import '../../../data/models/contact_state.dart';
+import '../../../data/models/payment_cubit.dart';
+import '../../ui_helpers.dart';
+import '../bottom_widget.dart';
+import '../header.dart';
+
+class ContactsPage extends StatefulWidget {
+  const ContactsPage({super.key});
+
+  @override
+  State<ContactsPage> createState() => _ContactsPageState();
+}
+
+class _ContactsPageState extends State<ContactsPage> {
+  final TextEditingController _searchController = TextEditingController();
+  late ContactsCubit _contactsCubit;
+
+  @override
+  void initState() {
+    super.initState();
+    _contactsCubit = context.read<ContactsCubit>();
+    _contactsCubit.resetFilter();
+  }
+
+  @override
+  void dispose() {
+    _searchController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // final ContactsCubit cubit = context.read<ContactsCubit>();
+    return BlocBuilder<ContactsCubit, ContactsState>(
+        builder: (BuildContext context, ContactsState state) {
+      return Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 16),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: <Widget>[
+              const Header(text: 'bottom_nav_trd'),
+              TextField(
+                controller: _searchController,
+                decoration: InputDecoration(
+                  hintText: tr('search_contacts'),
+                  border: const OutlineInputBorder(),
+                ),
+                onChanged: (String query) {
+                  _contactsCubit.filterContacts(query);
+                },
+              ),
+              if (state.filteredContacts.isEmpty)
+                const NoElements(text: 'no_contacts')
+              else
+                Expanded(
+                    child: ListView.builder(
+                  itemCount: state.filteredContacts.length,
+                  itemBuilder: (BuildContext context, int index) {
+                    final Contact contact = state.filteredContacts[index];
+                    return Slidable(
+                      // Specify a key if the Slidable is dismissible.
+                      key: const ValueKey<int>(0),
+
+                      // The start action pane is the one at the left or the top side.
+                      startActionPane: ActionPane(
+                        // A motion is a widget used to control how the pane animates.
+                        motion: const ScrollMotion(),
+
+                        // All actions are defined in the children parameter.
+                        children: <SlidableAction>[
+                          // A SlidableAction can have an icon and/or a label.
+                          SlidableAction(
+                            onPressed: (BuildContext c) {
+                              _contactsCubit.removeContact(contact);
+                            },
+                            backgroundColor: const Color(0xFFFE4A49),
+                            foregroundColor: Colors.white,
+                            icon: Icons.delete,
+                            label: tr('delete_contact'),
+                          ),
+                          /*  SlidableAction(
+                            onPressed: (BuildContext c) {},
+                            backgroundColor: const Color(0xFF21B7CA),
+                            foregroundColor: Colors.white,
+                            icon: Icons.share,
+                            label: tr('share_contact'),
+                          ),*/
+                        ],
+                      ),
+
+                      // The end action pane is the one at the right or the bottom side.
+                      endActionPane: ActionPane(
+                        motion: const ScrollMotion(),
+                        dismissible: DismissiblePane(onDismissed: () {
+                          onSent(context, contact);
+                        }),
+                        children: <SlidableAction>[
+                          SlidableAction(
+                            onPressed: (BuildContext c) {
+                              FlutterClipboard.copy(contact.pubkey).then(
+                                  (dynamic value) => ScaffoldMessenger.of(
+                                          context)
+                                      .showSnackBar(SnackBar(
+                                          content: Text(tr(
+                                              'some_key_copied_to_clipboard')))));
+                            },
+                            backgroundColor: Theme.of(context).primaryColorDark,
+                            foregroundColor: Colors.white,
+                            icon: Icons.copy,
+                            label: tr('copy_contact_key'),
+                          ),
+                          SlidableAction(
+                            onPressed: (BuildContext c) {
+                              onSent(c, contact);
+                            },
+                            backgroundColor: Theme.of(context).primaryColor,
+                            foregroundColor: Colors.white,
+                            icon: Icons.send,
+                            label: tr('send_g1'),
+                          ),
+                        ],
+                      ),
+                      child: ListTile(
+                        title: contact.nick != null
+                            ? Text(contact.nick!)
+                            : humanizePubKeyAsWidget(contact.pubkey),
+                        subtitle: contact.nick == null
+                            ? null
+                            : humanizePubKeyAsWidget(contact.pubkey),
+                        leading: avatar(
+                          contact.avatar != null,
+                          contact.avatar,
+                          bgColor: tileColor(index),
+                          color: tileColor(index, true),
+                        ),
+                        tileColor: tileColor(index),
+                      ),
+                    );
+                  },
+                )),
+              const BottomWidget()
+            ],
+          ));
+    });
+  }
+
+  void onSent(BuildContext c, Contact contact) {
+    c
+        .read<PaymentCubit>()
+        .selectUser(contact.pubkey, contact.nick, contact.avatar);
+    c.read<BottomNavCubit>().updateIndex(0);
+  }
+}
+
+class NoElements extends StatelessWidget {
+  const NoElements({super.key, required this.text});
+
+  final String text;
+
+  @override
+  Widget build(BuildContext context) {
+    return Expanded(child: Center(child: Text(tr(text))));
+  }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 5e6f831f..08af1b25 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -233,6 +233,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.1.1"
+  copy_with_extension:
+    dependency: "direct main"
+    description:
+      name: copy_with_extension
+      sha256: "13d2e7e1c4d420424db9137a5f595a9c624461e6abc5f71bd65d81e131fa6226"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.0.2"
+  copy_with_extension_gen:
+    dependency: "direct dev"
+    description:
+      name: copy_with_extension_gen
+      sha256: "2a22b974bdbd0b34ab5af230451799500e2c1c8e1759a117801f652b6c2a6c3b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.0.2"
   cron:
     dependency: "direct main"
     description:
@@ -435,6 +451,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.8"
+  flutter_slidable:
+    dependency: "direct main"
+    description:
+      name: flutter_slidable
+      sha256: "6c68e1fad129b4b807b2218ef4cf7f7f6f61c5ec8861c990dc2278d9d03cb09f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.0"
   flutter_svg:
     dependency: "direct main"
     description:
@@ -741,6 +765,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.0"
+  package_info_plus:
+    dependency: transitive
+    description:
+      name: package_info_plus
+      sha256: "8df5ab0a481d7dc20c0e63809e90a588e496d276ba53358afc4c4443d0a00697"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.3"
+  package_info_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: package_info_plus_platform_interface
+      sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
   path:
     dependency: transitive
     description:
@@ -965,6 +1005,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.27.7"
+  sentry:
+    dependency: transitive
+    description:
+      name: sentry
+      sha256: a1529c545fcbc899e5dcc7c94ff1c6ad0c334dfc99a3cda366b1da98af7c5678
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.22.0"
+  sentry_flutter:
+    dependency: "direct main"
+    description:
+      name: sentry_flutter
+      sha256: cab07e99a8f27af94f399cabceaff6968011660505b30a0e2286728a81bc476c
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.22.0"
   shared_preferences:
     dependency: "direct main"
     description:
@@ -1130,6 +1186,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.4.16"
+  timeago:
+    dependency: "direct main"
+    description:
+      name: timeago
+      sha256: "46c128312ab0ea144b146c0ac6426ddd96810efec2de3fccc425d00179cd8254"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.3.0"
   timing:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 75d5302a..d0125ef7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -55,6 +55,10 @@ dependencies:
   universal_html: ^2.0.9
   fl_chart: ^0.61.0
   json_annotation: ^4.8.0
+  flutter_slidable: ^2.0.0
+  sentry_flutter: ^6.22.0
+  copy_with_extension: ^5.0.2
+  timeago: ^3.3.0
 
 dev_dependencies:
   flutter_test:
@@ -62,6 +66,7 @@ dev_dependencies:
   flutter_lints: ^2.0.1
   build_runner: ^2.3.3
   json_serializable: ^6.6.1
+  copy_with_extension_gen: any
 
 # For information on the generic Dart part of this file, see the
 # following page: https://dart.dev/tools/pub/pubspec
diff --git a/test/keys_tests.dart b/test/keys_tests.dart
index 1a57587e..d5cfab50 100644
--- a/test/keys_tests.dart
+++ b/test/keys_tests.dart
@@ -2,6 +2,7 @@ import 'dart:typed_data';
 
 import 'package:durt/durt.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:ginkgo/data/models/payment_state.dart';
 import 'package:ginkgo/g1/g1_helper.dart';
 
 void main() {
@@ -20,6 +21,8 @@ void main() {
   });
 
   test('parse different networks/peers BMAS', () {
+    expect(
+        parseHost('BMAS g1.texu.es 7443'), equals('https://g1.texu.es:7443'));
     expect(parseHost('BMAS g1.duniter.org 443'),
         equals('https://g1.duniter.org:443'));
     expect(parseHost('BMAS g1.leprette.fr 443 /bma'),
@@ -30,7 +33,30 @@ void main() {
         parseHost(
             'BMAS monnaie-libre.ortie.org/bma/ 192.168.1.35 2a01:cb0d:5c2:fa00:21e:68ff:feab:389a 443'),
         equals('https://monnaie-libre.ortie.org:443/bma'));
-    expect(
-        parseHost('BMAS g1.texu.es 7443'), equals('https://g1.texu.es:7443'));
+  });
+
+  test('validate pub keys', () {
+    expect(validateKey('FRYyk57Pi456EJRu9vqVfSHLgmUfx4Qc3goS62a7dUSm'),
+        equals(true));
+
+    expect(validateKey('BrgsSYK3xUzDyztGBHmxq69gfNxBfe2UKpxG21oZUBr5'),
+        equals(true));
+  });
+
+  test('validate qr uris', () {
+    const String publicKey = 'FRYyk57Pi456EJRu9vqVfSHLgmUfx4Qc3goS62a7dUSm';
+    final String uriA = getQrUri(publicKey, '10');
+    final PaymentState? payA = parseScannedUri(uriA);
+    expect(payA!.amount, equals(10));
+    expect(payA.publicKey, equals(publicKey));
+
+    final String uriB = getQrUri(publicKey);
+    final PaymentState? payB = parseScannedUri(uriB);
+    expect(payB!.amount, equals(0));
+    expect(payB.publicKey, equals(publicKey));
+
+    final PaymentState? payC = parseScannedUri(publicKey);
+    expect(payC!.amount, equals(0));
+    expect(payC.publicKey, equals(publicKey));
   });
 }
diff --git a/test/transactions_tests.dart b/test/transactions_tests.dart
index bd8bff85..4d5efe7d 100644
--- a/test/transactions_tests.dart
+++ b/test/transactions_tests.dart
@@ -1,13 +1,13 @@
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:ginkgo/g1/transaction.dart';
+import 'package:ginkgo/data/models/transaction.dart';
 import 'package:ginkgo/g1/transaction_parser.dart';
 
 void main() {
   test('Test parsing', () async {
     TestWidgetsFlutterBinding.ensureInitialized();
     final String txData = await rootBundle.loadString('assets/tx.json');
-    final TransactionsAndBalance result = transactionParser(txData);
+    final TransactionsAndBalanceState result = transactionParser(txData);
     expect(result.balance, equals(6700));
     for (final Transaction tx in result.transactions) {
       expect(tx.from != tx.to, equals(true));
-- 
GitLab