diff --git a/lib/models/network_config.dart b/lib/models/network_config.dart new file mode 100644 index 0000000000000000000000000000000000000000..93e4ff544df09374d9ec98b549142ed096fc28ea --- /dev/null +++ b/lib/models/network_config.dart @@ -0,0 +1,16 @@ +class NetworkConfig { + final bool active; + final String genesisHash; + final int genesisTimestamp; + final int lastG1V1BlockNumber; + final List<String> rpc; + final List<String> squid; + + NetworkConfig.fromJson(Map<String, dynamic> json) + : active = json['active'] as bool, + genesisHash = json['genesis_hash'] as String, + genesisTimestamp = json['genesis_timestamp'] as int, + lastG1V1BlockNumber = json['last_g1_v1_block_number'] as int, + rpc = List<String>.from(json['rpc']), + squid = List<String>.from(json['squid']); +} diff --git a/lib/providers/duniter_indexer.dart b/lib/providers/duniter_indexer.dart index 83b28839884f618e62986288a21bd76ea29f0493..8cc88fcbd9596f4059192dd5571618904df9edaa 100644 --- a/lib/providers/duniter_indexer.dart +++ b/lib/providers/duniter_indexer.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:gecko/globals.dart'; import 'package:gecko/models/queries_indexer.dart'; @@ -11,6 +11,7 @@ import 'package:gecko/providers/substrate_sdk.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; import 'package:gecko/models/transaction.dart'; +import 'package:gecko/services/network_config_service.dart'; class DuniterIndexer with ChangeNotifier { Map<String, String?> walletNameIndexer = {}; @@ -65,14 +66,76 @@ class DuniterIndexer with ChangeNotifier { } } - Future<String> getValidIndexerEndpoint() async { - final homeProvider = Provider.of<HomeProvider>(homeContext, listen: false); + bool _isValidIndexerList(dynamic endpoints) { + if (endpoints == null) return false; + if (endpoints is! List) return false; + if (endpoints.isEmpty) return false; + return endpoints.every((e) => e is String); + } + + Future<List<String>> _fetchRemoteIndexerEndpoints() async { + try { + final config = await NetworkConfigService.getNetworkConfig(); + if (config.squid.isEmpty) throw 'No squid endpoints found'; + + // Nettoyer les URLs (retirer 'https://' et '/v1/graphql') + return config.squid.map((url) => url.replaceAll('https://', '').replaceAll('/v1/graphql', '')).toList(); + } catch (e) { + log.e('Erreur fetch remote indexer endpoints: $e'); + rethrow; + } + } + + Future<void> _updateIndexerEndpointsInBackground(List<String> currentEndpoints) async { + try { + final remoteEndpoints = await _fetchRemoteIndexerEndpoints(); + + // Comparer les listes sans tenir compte de l'ordre + final currentSet = Set.from(currentEndpoints); + final remoteSet = Set.from(remoteEndpoints); + + if (!setEquals(currentSet, remoteSet)) { + listIndexerEndpoints = remoteEndpoints; + log.i('Indexer endpoints mis à jour en background'); + } + } catch (e) { + log.e('Erreur update background indexer: $e'); + } + } + + Future<List<String>> _getBootstrapIndexerEndpoints() async { + // 1. Vérification rapide de la configBox + final existingEndpoints = configBox.get('squidNodes'); + if (_isValidIndexerList(existingEndpoints)) { + // Lancer la mise à jour en background + unawaited(_updateIndexerEndpointsInBackground(List<String>.from(existingEndpoints))); + return List<String>.from(existingEndpoints); + } + + try { + // 2. Tentative de fetch distant + final endpoints = await _fetchRemoteIndexerEndpoints(); + await configBox.put('squidNodes', endpoints); + return endpoints; + } catch (e) { + // 3. Fallback sur le fichier local + try { + final localEndpoints = await rootBundle.loadString('config/indexer_endpoints.json').then((jsonStr) => List<String>.from(jsonDecode(jsonStr))); - // await configBox.delete('indexerEndpoint'); + await configBox.put('squidNodes', localEndpoints); + return localEndpoints; + } catch (e) { + log.e('Erreur critique indexer endpoints: $e'); + return configBox.get('squidNodes') ?? []; + } + } + } - listIndexerEndpoints = await rootBundle.loadString('config/indexer_endpoints.json').then((jsonStr) => jsonDecode(jsonStr)); - // _listEndpoints.shuffle(); + Future<String> getValidIndexerEndpoint() async { + final homeProvider = Provider.of<HomeProvider>(homeContext, listen: false); + // Récupérer la liste des endpoints bootstrap + listIndexerEndpoints = await _getBootstrapIndexerEndpoints(); listIndexerEndpoints.add('Personnalisé'); if (configBox.containsKey('customIndexer')) { diff --git a/lib/providers/home.dart b/lib/providers/home.dart index cc2aa39739d361563ce6e155f8c0a284391f512d..2509abd44e7040dfad3c8b9155b3b4ee3951d2e8 100644 --- a/lib/providers/home.dart +++ b/lib/providers/home.dart @@ -9,7 +9,7 @@ import 'package:gecko/globals.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/providers/wallet_options.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:flutter/foundation.dart' show kDebugMode, kIsWeb; +import 'package:flutter/foundation.dart' show kDebugMode, kIsWeb, setEquals; import 'package:path_provider/path_provider.dart' as pp; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; @@ -19,6 +19,7 @@ import 'package:gecko/models/g1_wallets_list.dart'; import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/widgets/wallet_header.dart'; import 'package:gecko/models/wallet_header_data.dart'; +import 'package:gecko/services/network_config_service.dart'; class HomeProvider with ChangeNotifier { bool? isSearching; @@ -110,20 +111,75 @@ class HomeProvider with ChangeNotifier { } } - Future<List?> getValidEndpoints() async { - await configBox.delete('endpoint'); + bool _isValidEndpointsList(dynamic endpoints) { + if (endpoints == null) return false; + if (endpoints is! List) return false; + if (endpoints.isEmpty) return false; + return endpoints.every((e) => e is String && e.startsWith('ws')); + } + + Future<List<String>> _fetchRemoteEndpoints() async { + try { + final config = await NetworkConfigService.getNetworkConfig(); + if (config.rpc.isEmpty) throw 'No RPC endpoints found'; + return config.rpc; + } catch (e) { + log.e('Erreur fetch remote endpoints: $e'); + rethrow; + } + } + + Future<void> _updateEndpointsInBackground(List<String> currentEndpoints) async { + try { + final remoteEndpoints = await _fetchRemoteEndpoints(); + + // Comparer les listes sans tenir compte de l'ordre + final currentSet = Set.from(currentEndpoints); + final remoteSet = Set.from(remoteEndpoints); + + if (!setEquals(currentSet, remoteSet)) { + remoteEndpoints.shuffle(); + await configBox.put('endpoint', remoteEndpoints); + log.i('Endpoints mis à jour en background'); + } + } catch (e) { + log.e('Erreur update background: $e'); + } + } + + Future<List<String>> getValidEndpoints() async { + // 0. Set automode if not set if (!configBox.containsKey('autoEndpoint')) { configBox.put('autoEndpoint', true); } - List listEndpoints = []; - if (!configBox.containsKey('endpoint') || configBox.get('endpoint') == [] || configBox.get('endpoint') == '') { - listEndpoints = await rootBundle.loadString('config/gdev_endpoints.json').then((jsonStr) => jsonDecode(jsonStr)); - listEndpoints.shuffle(); - configBox.put('endpoint', listEndpoints); + // 1. Vérification rapide de la configBox + final existingEndpoints = configBox.get('endpoint'); + if (_isValidEndpointsList(existingEndpoints)) { + // Lancer la mise à jour en background + unawaited(_updateEndpointsInBackground(List<String>.from(existingEndpoints))); + return List<String>.from(existingEndpoints); } - return listEndpoints; + try { + // 2. Tentative de fetch distant + final endpoints = await _fetchRemoteEndpoints(); + endpoints.shuffle(); + await configBox.put('endpoint', endpoints); + return endpoints; + } catch (e) { + // 3. Fallback sur le fichier local + try { + final localEndpoints = await rootBundle.loadString('config/gdev_endpoints.json').then((jsonStr) => List<String>.from(jsonDecode(jsonStr))); + + localEndpoints.shuffle(); + await configBox.put('endpoint', localEndpoints); + return localEndpoints; + } catch (e) { + log.e('Erreur critique endpoints: $e'); + return configBox.get('endpoint') ?? []; + } + } } T getRandomElement<T>(List<T> list) { diff --git a/lib/services/network_config_service.dart b/lib/services/network_config_service.dart new file mode 100644 index 0000000000000000000000000000000000000000..b6889deb3ce24007bd874c041e61f6fe6649ea22 --- /dev/null +++ b/lib/services/network_config_service.dart @@ -0,0 +1,37 @@ +import 'dart:async'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:gecko/models/network_config.dart'; +import 'package:gecko/globals.dart'; + +class NetworkConfigService { + static NetworkConfig? _cachedConfig; + static DateTime? _lastFetchTime; + static const Duration _cacheValidity = Duration(minutes: 5); + static const String _configUrl = 'https://git.duniter.org/nodes/networks/-/raw/master/gdev.json'; + + static Future<NetworkConfig> getNetworkConfig() async { + // Retourner le cache s'il est valide + if (_cachedConfig != null && _lastFetchTime != null) { + final age = DateTime.now().difference(_lastFetchTime!); + if (age < _cacheValidity) { + return _cachedConfig!; + } + } + + try { + final response = await http.get(Uri.parse(_configUrl)).timeout(const Duration(seconds: 3)); + + if (response.statusCode != 200) { + throw 'Status code: ${response.statusCode}'; + } + + _cachedConfig = NetworkConfig.fromJson(jsonDecode(response.body)); + _lastFetchTime = DateTime.now(); + return _cachedConfig!; + } catch (e) { + log.e('Erreur fetch network config: $e'); + rethrow; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index df611b76a99f9b5863fb75dceb4858f394c6792d..2c035bc439a5bac129af6c124bf3b91b779ca221 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -774,13 +774,13 @@ packages: source: hosted version: "2.0.1" http: - dependency: transitive + dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 55039b72726492798d9df873a614a4403c275662..7281c1ae68c0245424b097e3772a0ffb83bb843f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: uuid: ^4.5.1 fade_and_translate: ^0.1.3 markdown: ^7.2.2 + http: ^1.3.0 # durt2: # path: ../../durt2 # git: