From 8bb54ad6dc56f2c1687a7df658989600d946f28e Mon Sep 17 00:00:00 2001
From: vjrj <vjrj@comunes.org>
Date: Sat, 25 Mar 2023 20:58:24 +0100
Subject: [PATCH] Cache of profiles (wip). Gva improvements

---
 lib/data/models/transaction.g.dart            |   5 +-
 lib/g1/api.dart                               |  49 ++-
 lib/g1/g1_helper.dart                         |   9 +-
 lib/ui/contacts_cache.dart                    |  85 +++++
 lib/ui/ui_helpers.dart                        |  30 ++
 .../first_screen/pay_contact_search_page.dart |  20 +-
 .../fourth_screen/transaction_item.dart       | 304 ++++++++++--------
 .../fourth_screen/transaction_page.dart       |  34 +-
 test/keys_test.dart                           |  12 +-
 9 files changed, 354 insertions(+), 194 deletions(-)
 create mode 100644 lib/ui/contacts_cache.dart

diff --git a/lib/data/models/transaction.g.dart b/lib/data/models/transaction.g.dart
index d7217394..6e57d124 100644
--- a/lib/data/models/transaction.g.dart
+++ b/lib/data/models/transaction.g.dart
@@ -250,7 +250,7 @@ Transaction _$TransactionFromJson(Map<String, dynamic> json) => Transaction(
 
 Map<String, dynamic> _$TransactionToJson(Transaction instance) =>
     <String, dynamic>{
-      'type': _$TransactionTypeEnumMap[instance.type],
+      'type': _$TransactionTypeEnumMap[instance.type]!,
       'from': instance.from,
       'to': instance.to,
       'amount': instance.amount,
@@ -262,8 +262,7 @@ Map<String, dynamic> _$TransactionToJson(Transaction instance) =>
       'fromNick': instance.fromNick,
     };
 
-const Map<TransactionType, String> _$TransactionTypeEnumMap =
-    <TransactionType, String>{
+const _$TransactionTypeEnumMap = {
   TransactionType.sending: 'sending',
   TransactionType.received: 'received',
   TransactionType.receiving: 'receiving',
diff --git a/lib/g1/api.dart b/lib/g1/api.dart
index 93b9f489..7564af17 100644
--- a/lib/g1/api.dart
+++ b/lib/g1/api.dart
@@ -13,6 +13,7 @@ import '../data/models/node_manager.dart';
 import '../data/models/node_type.dart';
 import '../shared_prefs.dart';
 import '../ui/logger.dart';
+import '../ui/ui_helpers.dart';
 import 'g1_helper.dart';
 
 // Tx history
@@ -47,6 +48,30 @@ Future<Response> searchCPlusUser(String searchTerm) async {
   return response;
 }
 
+Future<Contact> getProfile(String pubKey) async {
+  try {
+    final Response cPlusResponse = await requestCPlusWithRetry(
+        '/user/profile/$pubKey',
+        retryWith404: false);
+    final Map<String, dynamic> result =
+        const JsonDecoder().convert(cPlusResponse.body) as Map<String, dynamic>;
+    if (result['found'] == false) {
+      return Contact(pubkey: pubKey);
+    }
+
+    final String? nick = await gvaNick(pubKey);
+    final Map<String, dynamic> profile =
+        const JsonDecoder().convert(cPlusResponse.body) as Map<String, dynamic>;
+
+    final Contact c = contactFromResultSearch(profile);
+    logger('Contact retrieved in search $c');
+    return c.copyWith(nick: nick);
+  } catch (e) {
+    logger('Error in getProfile $e');
+    return Contact(pubkey: pubKey);
+  }
+}
+
 /*
 http://doc.e-is.pro/cesium-plus-pod/REST_API.html#userprofile
 Not found sample:
@@ -431,14 +456,15 @@ Future<String> pay(
   return output;
 }
 
-String getGvaNode() {
+String getGvaNode([bool useProxy = false]) {
   final List<Node> nodes = nodesWorkingList(NodeType.gva);
   if (nodes.isNotEmpty) {
     // reorder list to use others
     nodes.shuffle();
     // Reference of working proxy 'https://g1demo.comunes.net/proxy/g1v1.p2p.legal/gva/';
-    final String node =
-        'https://g1demo.comunes.net/proxy/${nodes.first.url.replaceFirst('https://', '').replaceFirst('http://', '')}/';
+    final String node = useProxy
+        ? 'https://g1demo.comunes.net/proxy/${nodes.first.url.replaceFirst('https://', '').replaceFirst('http://', '')}/'
+        : nodes.first.url;
     return node;
   } else {
     return 'Sorry: I cannot find a working node to send the transaction';
@@ -446,6 +472,21 @@ String getGvaNode() {
 }
 
 Future<Map<String, dynamic>?> gvaHistoryAndBalance(String pubKey) async {
+  return gvaFunctionWrapper<Map<String, dynamic>>(
+      pubKey, (Gva gva) => gva.history(pubKey));
+}
+
+Future<double?> gvaBalance(String pubKey) async {
+  return gvaFunctionWrapper<double>(pubKey, (Gva gva) => gva.balance(pubKey));
+}
+
+Future<String?> gvaNick(String pubKey) async {
+  return gvaFunctionWrapper<String>(
+      pubKey, (Gva gva) => gva.getUsername(pubKey));
+}
+
+Future<T?> gvaFunctionWrapper<T>(
+    String pubKey, Future<T?> Function(Gva) specificFunction) async {
   final List<Node> nodes = NodeManager().nodeList(NodeType.gva);
   if (nodes.isEmpty) {
     nodes.addAll(defaultGvaNodes);
@@ -461,7 +502,7 @@ Future<Map<String, dynamic>?> gvaHistoryAndBalance(String pubKey) async {
       if (Uri.tryParse(output) != null) {
         final String node = output;
         final Gva gva = Gva(node: node);
-        final Map<String, dynamic>? result = await gva.history(pubKey);
+        final T? result = await specificFunction(gva);
         return result;
       }
     } catch (e) {
diff --git a/lib/g1/g1_helper.dart b/lib/g1/g1_helper.dart
index 9224d576..490c4306 100644
--- a/lib/g1/g1_helper.dart
+++ b/lib/g1/g1_helper.dart
@@ -60,7 +60,7 @@ String? parseHost(String endpointUnParsed) {
     /* print(lastPart);
     print(path);
     print(nextToLast); */
-    final String port = path == ''
+    String port = path == ''
         ? (RegExp(r'^[0-9]+$').hasMatch(lastPart) ? lastPart : '443')
         : RegExp(r'^[0-9]+$').hasMatch(nextToLast)
             ? nextToLast
@@ -81,7 +81,12 @@ String? parseHost(String endpointUnParsed) {
     if (endpointUnParsed.endsWith('gva')) {
       path = '/gva';
     }
-    final String endpoint = '$protocol://$host:$port$path'.trim();
+    if (port == '443') {
+      port = '';
+    } else {
+      port = ':$port';
+    }
+    final String endpoint = '$protocol://$host$port$path'.trim();
     return endpoint;
   } catch (e) {
     // Don't do this here or tests will fail
diff --git a/lib/ui/contacts_cache.dart b/lib/ui/contacts_cache.dart
new file mode 100644
index 00000000..8568ff63
--- /dev/null
+++ b/lib/ui/contacts_cache.dart
@@ -0,0 +1,85 @@
+import 'dart:convert';
+import 'dart:html';
+
+import 'package:flutter/foundation.dart';
+
+import '../data/models/contact.dart';
+import '../g1/api.dart';
+import 'logger.dart';
+
+class ContactsCacheBasic {
+  factory ContactsCacheBasic() {
+    _instance ??= ContactsCacheBasic._internal();
+    return _instance!;
+  }
+
+  ContactsCacheBasic._internal();
+
+  static ContactsCacheBasic? _instance;
+
+  final Map<String, Contact> _cache = <String, Contact>{};
+
+  Future<Contact> getContact(String pubKey) async {
+    final String cacheKey = 'avatar-$pubKey';
+
+    final Contact? cachedContact = _cache[cacheKey];
+    if (cachedContact != null) {
+      return cachedContact;
+    }
+
+    final Contact contact = await getProfile(pubKey);
+
+    _cache[cacheKey] = contact;
+
+    return contact;
+  }
+}
+
+class ContactsCache {
+  factory ContactsCache() {
+    _instance ??= ContactsCache._internal();
+    return _instance!;
+  }
+
+  ContactsCache._internal();
+
+  static ContactsCache? _instance;
+
+  Future<Contact> getContact(String pubKey) async {
+    final String cacheKey = 'avatar-$pubKey';
+    const Duration duration = Duration(days: 3);
+
+    try {
+      final String? cachedValue = window.localStorage[cacheKey];
+      if (cachedValue != null) {
+        final Map<String, dynamic> decodedValue =
+        json.decode(cachedValue) as Map<String, dynamic>;
+        final DateTime timestamp =
+        DateTime.parse(decodedValue['timestamp'] as String);
+
+        if (DateTime.now().isBefore(timestamp.add(duration))) {
+          final Contact contact =
+          Contact.fromJson(decodedValue['data'] as Map<String, dynamic>);
+          if (!kReleaseMode) {
+            logger('Returning cached contact $contact');
+          }
+          return contact;
+        }
+      }
+    } catch (e) {
+      logger('Error while retrieving contact from cache: $e, $pubKey');
+    }
+
+    final Contact contact = await getProfile(pubKey);
+
+    final String encodedValue = json.encode(<String, dynamic>{
+      'timestamp': DateTime.now().toIso8601String(),
+      'data': contact.toJson(),
+    });
+    window.localStorage[cacheKey] = encodedValue;
+    if (!kReleaseMode) {
+      logger('Returning non cached contact $contact');
+    }
+    return contact;
+  }
+}
diff --git a/lib/ui/ui_helpers.dart b/lib/ui/ui_helpers.dart
index 3ae8976f..c9658096 100644
--- a/lib/ui/ui_helpers.dart
+++ b/lib/ui/ui_helpers.dart
@@ -5,7 +5,9 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:timeago/timeago.dart' as timeago;
 
+import '../data/models/contact.dart';
 import '../data/models/transaction_type.dart';
+import '../g1/api.dart';
 import '../shared_prefs.dart';
 import 'widgets/first_screen/circular_icon.dart';
 
@@ -117,3 +119,31 @@ bool isOutgoing(TransactionType type) {
 bool isIncoming(TransactionType type) {
   return type == TransactionType.receiving || type == TransactionType.received;
 }
+
+Contact contactFromResultSearch(Map<String, dynamic> record) {
+  final Map<String, dynamic> source = record['_source'] as Map<String, dynamic>;
+  final Uint8List? avatarBase64 = _getAvatarFromResults(source);
+  return Contact(
+      pubkey: record['_id'] as String,
+      name: source['title'] as String,
+      avatar: avatarBase64);
+}
+
+Contact contactFromUserProfile(Map<String, dynamic> source) {
+  final Uint8List? avatarBase64 = _getAvatarFromResults(source);
+  return Contact(
+      pubkey: source['issuer'] as String,
+      name: source['title'] as String,
+      avatar: avatarBase64);
+}
+
+Uint8List? _getAvatarFromResults(Map<String, dynamic> source) {
+  Uint8List? avatarBase64;
+  if (source['avatar'] != null) {
+    final Map<String, dynamic> avatar =
+        source['avatar'] as Map<String, dynamic>;
+    avatarBase64 = imageFromBase64String(
+        'data:${avatar['_content_type']};base64,${avatar['_content']}');
+  }
+  return avatarBase64;
+}
diff --git a/lib/ui/widgets/first_screen/pay_contact_search_page.dart b/lib/ui/widgets/first_screen/pay_contact_search_page.dart
index 31adea51..9f32790a 100644
--- a/lib/ui/widgets/first_screen/pay_contact_search_page.dart
+++ b/lib/ui/widgets/first_screen/pay_contact_search_page.dart
@@ -1,7 +1,6 @@
 import 'dart:convert';
 
 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:http/http.dart';
@@ -54,7 +53,8 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> {
                 .convert(cPlusResponse.body) as Map<String, dynamic>)['hits']
             as Map<String, dynamic>)['hits'] as List<dynamic>;
         for (final dynamic hit in hits) {
-          final Contact c = _contactFromResult(hit as Map<String, dynamic>);
+          final Contact c =
+              contactFromResultSearch(hit as Map<String, dynamic>);
           logger('Contact retrieved in search $c');
           _results.add(c);
         }
@@ -234,20 +234,4 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> {
       }),
     );
   }
-
-  Contact _contactFromResult(Map<String, dynamic> record) {
-    final Map<String, dynamic> source =
-        record['_source'] as Map<String, dynamic>;
-    Uint8List? avatarBase64;
-    if (source['avatar'] != null) {
-      final Map<String, dynamic> avatar =
-          source['avatar'] as Map<String, dynamic>;
-      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/fourth_screen/transaction_item.dart b/lib/ui/widgets/fourth_screen/transaction_item.dart
index 81b6dcab..4d6f23cf 100644
--- a/lib/ui/widgets/fourth_screen/transaction_item.dart
+++ b/lib/ui/widgets/fourth_screen/transaction_item.dart
@@ -3,110 +3,116 @@ 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/transaction.dart';
 import '../../../data/models/transaction_cubit.dart';
 import '../../../data/models/transaction_type.dart';
 import '../../../shared_prefs.dart';
+import '../../contacts_cache.dart';
 import '../../ui_helpers.dart';
 
 class TransactionListItem extends StatelessWidget {
   const TransactionListItem({
     super.key,
+    required this.pubKey,
     required this.transaction,
-    required this.avatar,
     required this.index,
   });
 
+  final String pubKey;
   final Transaction transaction;
-  final Widget avatar;
   final int index;
 
   @override
-  Widget build(BuildContext context) =>
-      BlocBuilder<TransactionsCubit, TransactionsAndBalanceState>(builder:
-          (BuildContext context,
-              TransactionsAndBalanceState transBalanceState) {
-        IconData? icon;
-        Color? iconColor;
-        String statusText;
-        final String amountS =
-            '${transaction.amount < 0 ? "" : "+"}${formatKAmount(context, transaction.amount)}';
-        statusText = tr('transaction_${transaction.type.name}');
-        switch (transaction.type) {
-          case TransactionType.pending:
-            icon = Icons.timelapse;
-            iconColor = Colors.grey;
-            break;
-          case TransactionType.sending:
-            icon = Icons.flight_takeoff;
-            iconColor = Colors.grey;
-            break;
-          case TransactionType.receiving:
-            icon = Icons.flight_land;
-            iconColor = Colors.grey;
-            break;
-          case TransactionType.sent:
-            break;
-          case TransactionType.received:
-            break;
-        }
-        final String myPubKey = SharedPreferencesHelper().getPubKey();
-        final ContactsCubit contactsCubit = context.read<ContactsCubit>();
-        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);
-                      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'),
-                ),
-              ],
-            ),
-            child: ListTile(
-              leading: (icon != null)
-                  ? Icon(
-                      icon,
-                      color: iconColor,
-                    )
-                  : null,
-              tileColor: tileColor(index, context),
-              title: Row(
-                children: <Widget>[
-                  // if (avatar != null) avatar,
-                  const SizedBox(width: 8.0),
-                  Expanded(
-                    child: Column(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      children: <Widget>[
-                        Text(
-                          statusText,
-                          style: const TextStyle(
-                            fontSize: 12.0,
-                            color: Colors.grey,
+  Widget build(BuildContext context) => BlocBuilder<TransactionsCubit,
+          TransactionsAndBalanceState>(
+      builder: (BuildContext context,
+              TransactionsAndBalanceState transBalanceState) =>
+          FutureBuilder<Contact>(
+              future: _fetchContact(pubKey, transaction),
+              builder: (BuildContext context, AsyncSnapshot<Contact> snapshot) {
+                if (snapshot.hasData) {
+                  IconData? icon;
+                  Color? iconColor;
+                  String statusText;
+                  final String amountS =
+                      '${transaction.amount < 0 ? "" : "+"}${formatKAmount(context, transaction.amount)}';
+                  statusText = tr('transaction_${transaction.type.name}');
+
+                  switch (transaction.type) {
+                    case TransactionType.pending:
+                      icon = Icons.timelapse;
+                      iconColor = Colors.grey;
+                      break;
+                    case TransactionType.sending:
+                      icon = Icons.flight_takeoff;
+                      iconColor = Colors.grey;
+                      break;
+                    case TransactionType.receiving:
+                      icon = Icons.flight_land;
+                      iconColor = Colors.grey;
+                      break;
+                    case TransactionType.sent:
+                      break;
+                    case TransactionType.received:
+                      break;
+                  }
+                  final String myPubKey = SharedPreferencesHelper().getPubKey();
+                  final ContactsCubit contactsCubit =
+                      context.read<ContactsCubit>();
+                  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) {
+                              if (snapshot.hasData)
+                                contactsCubit.addContact(snapshot.data!);
+                              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'),
                           ),
-                        ),
-                        const SizedBox(height: 4.0),
-                        Text.rich(
-                          TextSpan(
-                            children: <InlineSpan>[
-                              /* TextSpan(
+                        ],
+                      ),
+                      child: ListTile(
+                        leading: (icon != null)
+                            ? Icon(
+                                icon,
+                                color: iconColor,
+                              )
+                            : null,
+                        tileColor: tileColor(index, context),
+                        title: Row(
+                          children: <Widget>[
+                            // if (avatar != null) avatar,
+                            const SizedBox(width: 8.0),
+                            Expanded(
+                              child: Column(
+                                crossAxisAlignment: CrossAxisAlignment.start,
+                                children: <Widget>[
+                                  Text(
+                                    statusText,
+                                    style: const TextStyle(
+                                      fontSize: 12.0,
+                                      color: Colors.grey,
+                                    ),
+                                  ),
+                                  const SizedBox(height: 4.0),
+                                  Text.rich(
+                                    TextSpan(
+                                      children: <InlineSpan>[
+                                        /* TextSpan(
                                 text: isIncoming(transaction.type)
                                     ? 'Recibido de '
                                     : 'Pago a ',
@@ -115,66 +121,84 @@ class TransactionListItem extends StatelessWidget {
                                   // fontWeight: FontWeight.bold,
                                 ),
                               ), */
-                              /* WidgetSpan(
+                                        /* WidgetSpan(
                                 child: avatar != null
                                     ? const SizedBox(width: 8.0)
                                     : const SizedBox.shrink(),
                               ), */
-                              WidgetSpan(
-                                child: Text(
-                                  tr('transaction_from_to',
-                                      namedArgs: <String, String>{
-                                        'from': humanizeFromToPubKey(
-                                            myPubKey, transaction.from),
-                                        'to': humanizeFromToPubKey(
-                                            myPubKey, transaction.to)
-                                      }),
-                                  style: const TextStyle(
-                                    fontSize: 14.0,
-                                    // fontWeight: FontWeight.bold,
+                                        WidgetSpan(
+                                          child: Text(
+                                            tr('transaction_from_to',
+                                                namedArgs: <String, String>{
+                                                  'from': humanizeFromToPubKey(
+                                                      myPubKey,
+                                                      transaction.from),
+                                                  'to': humanizeFromToPubKey(
+                                                      myPubKey, transaction.to)
+                                                }),
+                                            style: const TextStyle(
+                                              fontSize: 14.0,
+                                              // fontWeight: FontWeight.bold,
+                                            ),
+                                          ),
+                                        ),
+                                      ],
+                                    ),
                                   ),
-                                ),
+                                ],
                               ),
-                            ],
-                          ),
+                            ),
+                          ],
+                        ),
+                        subtitle: Padding(
+                          padding: const EdgeInsets.fromLTRB(8, 0, 0, 10),
+                          child: Text(transaction.comment,
+                              style: const TextStyle(
+                                fontStyle: FontStyle.italic,
+                                color: Colors.grey,
+                              )),
+                        ),
+                        trailing: Column(
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          crossAxisAlignment: CrossAxisAlignment.end,
+                          children: <Widget>[
+                            Text(
+                              amountS,
+                              style: TextStyle(
+                                // fontWeight: FontWeight.bold,
+                                color: transaction.type ==
+                                            TransactionType.received ||
+                                        transaction.type ==
+                                            TransactionType.receiving
+                                    ? Colors.blue
+                                    : Colors.red,
+                              ),
+                            ),
+                            const SizedBox(height: 4.0),
+                            Text(
+                              humanizeTime(
+                                  transaction.time, context.locale.toString())!,
+                              style: const TextStyle(
+                                fontSize: 12.0,
+                                color: Colors.grey,
+                              ),
+                            ),
+                          ],
                         ),
-                      ],
-                    ),
-                  ),
-                ],
-              ),
-              subtitle: Padding(
-                padding: const EdgeInsets.fromLTRB(8, 0, 0, 10),
-                child: Text(transaction.comment,
-                    style: const TextStyle(
-                      fontStyle: FontStyle.italic,
-                      color: Colors.grey,
-                    )),
-              ),
-              trailing: Column(
-                mainAxisAlignment: MainAxisAlignment.center,
-                crossAxisAlignment: CrossAxisAlignment.end,
-                children: <Widget>[
-                  Text(
-                    amountS,
-                    style: TextStyle(
-                      // fontWeight: FontWeight.bold,
-                      color: transaction.type == TransactionType.received ||
-                              transaction.type == TransactionType.receiving
-                          ? Colors.blue
-                          : Colors.red,
-                    ),
-                  ),
-                  const SizedBox(height: 4.0),
-                  Text(
-                    humanizeTime(transaction.time, context.locale.toString())!,
-                    style: const TextStyle(
-                      fontSize: 12.0,
-                      color: Colors.grey,
-                    ),
-                  ),
-                ],
-              ),
-            ));
-      });
+                      ));
+                } else if (snapshot.hasError) {
+                  return Text('Error ${snapshot.error}');
+                } else {
+                  return const Text('Loading');
+                }
+              }));
+
+  Future<Contact> _fetchContact(String pubKey, Transaction transaction) async {
+    return Contact(pubkey: pubKey);
+    if (pubKey == transaction.from) {
+      return ContactsCache().getContact(transaction.to);
+    } else {
+      return ContactsCache().getContact(transaction.from);
+    }
+  }
 }
diff --git a/lib/ui/widgets/fourth_screen/transaction_page.dart b/lib/ui/widgets/fourth_screen/transaction_page.dart
index 523a2ec7..1e9965c7 100644
--- a/lib/ui/widgets/fourth_screen/transaction_page.dart
+++ b/lib/ui/widgets/fourth_screen/transaction_page.dart
@@ -5,11 +5,12 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import '../../../data/models/contact.dart';
-import '../../../data/models/contact_cubit.dart';
 import '../../../data/models/node_list_cubit.dart';
 import '../../../data/models/transaction.dart';
 import '../../../data/models/transaction_cubit.dart';
 import '../../../shared_prefs.dart';
+import '../../contacts_cache.dart';
+import '../../logger.dart';
 import '../../ui_helpers.dart';
 import 'transaction_chart.dart';
 import 'transaction_item.dart';
@@ -48,10 +49,7 @@ class _TransactionsAndBalanceWidgetState
   }
 
   Future<void> _scrollListener() async {
-    if (/* _transScrollController.position.pixels ==
-             _transScrollController.position.maxScrollExtent || */
-        _transScrollController.offset == 0) {
-      // await _refreshTransactions();
+    if (_transScrollController.offset == 0) {
       _refreshIndicatorKey.currentState?.show();
     }
   }
@@ -66,11 +64,17 @@ class _TransactionsAndBalanceWidgetState
   @override
   Widget build(BuildContext context) {
     final String myPubKey = SharedPreferencesHelper().getPubKey();
+    if (!kReleaseMode) {
+      ContactsCache()
+          .getContact('6DrGg8cftpkgffv4Y4Lse9HSjgc8coEQor3yvMPHAnVH');
+      ContactsCache()
+          .getContact('A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB');
+      ContactsCache()
+          .getContact('6DrGg8cftpkgffv4Y4Lse9HSjgc8coEQor3yvMPHAnVH');
+      ContactsCache().getContact(myPubKey).then((Contact c) => logger(c));
+    }
     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 double balance = transBalanceState.balance;
       return BackdropScaffold(
@@ -172,9 +176,9 @@ class _TransactionsAndBalanceWidgetState
                               // itemExtent: 100,
                               itemBuilder: (BuildContext context, int index) {
                                 return TransactionListItem(
+                                  pubKey: myPubKey,
                                   index: index,
                                   transaction: transactions[index],
-                                  avatar: avatar(null),
                                 );
                                 /*
                                    Slidable(
@@ -251,16 +255,4 @@ class _TransactionsAndBalanceWidgetState
           ));
     });
   }
-
-  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/test/keys_test.dart b/test/keys_test.dart
index cf31bd91..6936a404 100644
--- a/test/keys_test.dart
+++ b/test/keys_test.dart
@@ -24,21 +24,21 @@ 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.duniter.org 443'), equals('https://g1.duniter.org'));
     expect(parseHost('BMAS g1.leprette.fr 443 /bma'),
-        equals('https://g1.leprette.fr:443/bma'));
+        equals('https://g1.leprette.fr/bma'));
     expect(parseHost('BMAS g1-vijitatman.es 212.227.41.252 443'),
-        equals('https://g1-vijitatman.es:443'));
+        equals('https://g1-vijitatman.es'));
     expect(
         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'));
+        equals('https://monnaie-libre.ortie.org/bma'));
   });
 
   test('parse different networks/peers GVA S', () {
     expect(parseHost('GVA S duniter.master.aya.autissier.net 443 gva'),
-        equals('https://duniter.master.aya.autissier.net:443/gva'));
+        equals('https://duniter.master.aya.autissier.net/gva'));
   });
 
   test('validate pub keys', () {
-- 
GitLab