From d61ffe6a9ac879a7678e8ca0b996913b8deec0ee Mon Sep 17 00:00:00 2001 From: vjrj <vjrj@comunes.org> Date: Sun, 5 Mar 2023 20:42:37 +0100 Subject: [PATCH] More work in node manager --- .../comunes/ginkgo}/MainActivity.kt | 2 +- lib/config/{config.dart => api.dart} | 11 +-- lib/g1/duniter_node_manager.dart | 81 +++++++++++++------ lib/main.dart | 13 ++- lib/ui/screens/fifth_screen.dart | 4 +- lib/ui/screens/first_screen.dart | 9 ++- lib/ui/widgets/fifth_screen/info_card.dart | 9 ++- .../first_screen/contact_search_dialog.dart | 2 +- pubspec.lock | 8 ++ pubspec.yaml | 1 + 10 files changed, 101 insertions(+), 39 deletions(-) rename android/app/src/main/kotlin/{dev/feichtinger/flutterproductionboilerplate/flutter_production_boilerplate => org/comunes/ginkgo}/MainActivity.kt (53%) rename lib/config/{config.dart => api.dart} (70%) diff --git a/android/app/src/main/kotlin/dev/feichtinger/flutterproductionboilerplate/flutter_production_boilerplate/MainActivity.kt b/android/app/src/main/kotlin/org/comunes/ginkgo/MainActivity.kt similarity index 53% rename from android/app/src/main/kotlin/dev/feichtinger/flutterproductionboilerplate/flutter_production_boilerplate/MainActivity.kt rename to android/app/src/main/kotlin/org/comunes/ginkgo/MainActivity.kt index a54c301d..8b493f9b 100644 --- a/android/app/src/main/kotlin/dev/feichtinger/flutterproductionboilerplate/flutter_production_boilerplate/MainActivity.kt +++ b/android/app/src/main/kotlin/org/comunes/ginkgo/MainActivity.kt @@ -1,4 +1,4 @@ -package dev.feichtinger.flutterproductionboilerplate.flutter_production_boilerplate +package org.comunes.ginkgo import io.flutter.embedding.android.FlutterActivity diff --git a/lib/config/config.dart b/lib/config/api.dart similarity index 70% rename from lib/config/config.dart rename to lib/config/api.dart index d9fcb03b..5802ff3f 100644 --- a/lib/config/config.dart +++ b/lib/config/api.dart @@ -1,21 +1,22 @@ -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; +import '../g1/duniter_node_manager.dart'; + String get duniterNet { - return dotenv.get('NET'); + return DuniterNodeManager().fastestNode; } String get duniterLookupUrl { - return '${duniterNet}wot/lookup/'; + return '${duniterNet}/wot/lookup/'; } String get duniterNetworkPeers { - return '${duniterNet}network/peers'; + return '${duniterNet}/network/peers'; } String duniterAccountAvatar(String publickey) { - return '${duniterNet}node/peers/$publickey/avatar'; + return '${duniterNet}/node/peers/$publickey/avatar'; } Future<String> getAvatar(String publicKey) async { diff --git a/lib/g1/duniter_node_manager.dart b/lib/g1/duniter_node_manager.dart index 7b60a174..146ced14 100644 --- a/lib/g1/duniter_node_manager.dart +++ b/lib/g1/duniter_node_manager.dart @@ -1,19 +1,33 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; import '../main.dart'; class DuniterNodeManager { - DuniterNodeManager() { - _loadNodes(); + factory DuniterNodeManager() { + return _instance; + } + + DuniterNodeManager._internal() { _startResetErrorsTimer(); } - final String _peerListUrl = 'https://nodes.duniter.org/network/peers'; - List<String> _nodes = <String>[]; + void init() { + loadNodes(); + } + + String get fastestNode { + return _fastestNode!; + } + + static final DuniterNodeManager _instance = DuniterNodeManager._internal(); + + final String _peerListUrl = 'https://g1.duniter.org/network/peers'; + final List<String> _nodes = <String>[]; int _currentNodeIndex = 0; final int _retryCount = 3; Map<String, int> _nodeErrors = <String, int>{}; @@ -42,22 +56,55 @@ class DuniterNodeManager { throw Exception('No nodes available'); } - Future<void> _loadNodes() async { + Future<void> loadNodes() async { try { final Response response = await http.get(Uri.parse(_peerListUrl)); if (response.statusCode == 200) { - final List<dynamic> peerList = - jsonDecode(response.body) as List<dynamic>; - _nodes = peerList + final Map<String, dynamic> peerList = + jsonDecode(response.body) as Map<String, dynamic>; + final List<dynamic> peers = (peerList['peers'] as List<dynamic>) .where((dynamic peer) => (peer as Map<String, dynamic>)['currency'] == 'g1') - .map((dynamic peer) => - 'http://${(peer as Map<String, dynamic>)['host']}:${peer['port']}/') + .where((dynamic peer) => + (peer as Map<String, dynamic>)['version'] == 10) + .where((dynamic peer) => + (peer as Map<String, dynamic>)['status'] == 'UP') .toList(); + for (final dynamic peer in peers) { + if (peer['endpoints'] != null) { + final List<String> endpoints = + List<String>.from(peer['endpoints'] as List<dynamic>); + for (int j = 0; j < endpoints.length; j++) { + if (endpoints[j].startsWith('BMAS')) { + String endpoint = endpoints[j].replaceAll('BMAS ', ''); + if (endpoint.contains(' ')) { + endpoint = endpoint.substring(0, endpoint.indexOf(' ')); + } + endpoint = 'https://${endpoint.replaceAll(':443', '')}'; + _nodes.add(endpoint); + + final Duration latency = await _pingNode(endpoint); + + if (_fastestNode == null || latency < _fastestLatency!) { + _fastestNode = endpoint; + _fastestLatency = latency; + if (!kReleaseMode) { + logger('Current faster node $_fastestNode'); + } + } + } + } + } + } _resetNodeErrors(null); } + logger('Loaded ${_nodes.length} duniter nodes'); + if (!kReleaseMode) { + logger(_nodes); + } } catch (e) { logger('Error: $e'); + rethrow; } } @@ -95,19 +142,7 @@ class DuniterNodeManager { _cancelResetErrorsTimer(); } - Future<String?> getFastestNode() async { - for (final String node in _nodes) { - final Duration latency = await pingNode(node); - - if (_fastestNode == null || latency < _fastestLatency!) { - _fastestNode = node; - _fastestLatency = latency; - } - } - return _fastestNode; - } - - Future<Duration> pingNode(String node) async { + Future<Duration> _pingNode(String node) async { try { final Stopwatch stopwatch = Stopwatch()..start(); await http.get(Uri.parse('$node/network/peers/self/ping')); diff --git a/lib/main.dart b/lib/main.dart index 94068bfe..d6037915 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:connectivity_wrapper/connectivity_wrapper.dart'; +import 'package:cron/cron.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:easy_logger/easy_logger.dart'; import 'package:flutter/foundation.dart'; @@ -16,6 +17,7 @@ import 'package:responsive_framework/responsive_wrapper.dart'; import 'config/theme.dart'; import 'cubit/theme_cubit.dart'; +import 'g1/duniter_node_manager.dart'; import 'shared_prefs.dart'; import 'ui/screens/skeleton_screen.dart'; @@ -67,6 +69,13 @@ void main() async { await HydratedStorage.build(storageDirectory: tmpDir); } + final Cron cron = Cron(); + cron.schedule(Schedule.parse('*/45 * * * *'), () async { + // Every 45m check for faster node (maybe it something costly in terms of + // bandwidth + DuniterNodeManager().loadNodes(); + }); + runApp( EasyLocalization( path: 'assets/translations', @@ -153,7 +162,7 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State<MyApp> { - final bool _skip = false; + final bool _skipIntro = !kReleaseMode; @override Widget build(BuildContext context) { @@ -178,7 +187,7 @@ class _MyAppState extends State<MyApp> { debugShowCheckedModeBanner: false, home: MediaQuery( data: const MediaQueryData(), - child: _skip ? const SkeletonScreen() : const AppIntro(), + child: _skipIntro ? const SkeletonScreen() : const AppIntro(), ), builder: (BuildContext buildContext, Widget? widget) { return ResponsiveWrapper.builder( diff --git a/lib/ui/screens/fifth_screen.dart b/lib/ui/screens/fifth_screen.dart index 5b05fbfc..0b64e275 100644 --- a/lib/ui/screens/fifth_screen.dart +++ b/lib/ui/screens/fifth_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import '../../config/config.dart'; +import '../../config/api.dart'; import '../widgets/fifth_screen/grid_item.dart'; import '../widgets/fifth_screen/info_card.dart'; import '../widgets/fifth_screen/link_card.dart'; @@ -20,7 +20,7 @@ class FifthScreen extends StatelessWidget { physics: const BouncingScrollPhysics(), children: <Widget>[ const Header(text: 'bottom_nav_fifth'), - InfoCard(title: duniterNet, icon: Icons.hub), + InfoCard(title: duniterNet, icon: Icons.hub, translate: false), LinkCard( title: 'code_card_title', icon: Icons.code_rounded, diff --git a/lib/ui/screens/first_screen.dart b/lib/ui/screens/first_screen.dart index 68f2f459..770bc8d3 100644 --- a/lib/ui/screens/first_screen.dart +++ b/lib/ui/screens/first_screen.dart @@ -1,7 +1,9 @@ import 'package:another_flushbar/flushbar.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../../g1/duniter_node_manager.dart'; import '../widgets/first_screen/credit_card.dart'; import '../widgets/first_screen/pay_contact_search_bar.dart'; import '../widgets/header.dart'; @@ -19,9 +21,9 @@ class _FirstScreenState extends State<FirstScreen> { @override Widget build(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - // ignore: always_specify_types - if (_showFlushbar) + WidgetsBinding.instance.addPostFrameCallback((_) async { + DuniterNodeManager().init(); + if (_showFlushbar && kReleaseMode) { Flushbar<void>( message: tr('demo-title'), title: tr('demo-desc'), @@ -37,6 +39,7 @@ class _FirstScreenState extends State<FirstScreen> { } }, ).show(context); + } }); return Material( color: Theme.of(context).colorScheme.background, diff --git a/lib/ui/widgets/fifth_screen/info_card.dart b/lib/ui/widgets/fifth_screen/info_card.dart index dcb4ed3f..722fef80 100644 --- a/lib/ui/widgets/fifth_screen/info_card.dart +++ b/lib/ui/widgets/fifth_screen/info_card.dart @@ -3,10 +3,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; class InfoCard extends StatelessWidget { - const InfoCard({super.key, required this.title, required this.icon}); + const InfoCard( + {super.key, + required this.title, + required this.icon, + this.translate = true}); final String title; final IconData icon; + final bool translate; @override Widget build(BuildContext context) { @@ -32,7 +37,7 @@ class InfoCard extends StatelessWidget { Icon(icon, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 16), Text( - tr(title), + translate ? tr(title) : title, style: Theme.of(context) .textTheme .titleMedium! diff --git a/lib/ui/widgets/first_screen/contact_search_dialog.dart b/lib/ui/widgets/first_screen/contact_search_dialog.dart index c28de21b..80f42675 100644 --- a/lib/ui/widgets/first_screen/contact_search_dialog.dart +++ b/lib/ui/widgets/first_screen/contact_search_dialog.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:simple_barcode_scanner/simple_barcode_scanner.dart'; -import '../../../config/config.dart'; +import '../../../config/api.dart'; import 'circular_icon.dart'; class SearchDialog extends StatefulWidget { diff --git a/pubspec.lock b/pubspec.lock index bbae2101..fd692c47 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cron: + dependency: "direct main" + description: + name: cron + sha256: d98aa8cdad0cccdb6b098e6a1fb89339c180d8a229145fa4cd8c6fc538f0e35f + url: "https://pub.dev" + source: hosted + version: "0.5.1" crypto: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2ff6f3a7..656c3fc9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: shared_preferences: ^2.0.18 another_flushbar: ^1.12.29 vibration: ^1.7.6 + cron: ^0.5.1 dev_dependencies: flutter_test: -- GitLab