diff --git a/config/indexer_endpoints.json b/config/indexer_endpoints.json index feb604e..eac0224 100644 --- a/config/indexer_endpoints.json +++ b/config/indexer_endpoints.json @@ -1,4 +1,5 @@ [ + "https://idx.gdev.cgeek.fr", "https://duniter-indexer.coinduf.eu", "http://192.168.1.72:8080" ] diff --git a/lib/main.dart b/lib/main.dart index 210b257..148d336 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,6 +25,7 @@ import 'package:gecko/providers/chest_provider.dart'; import 'package:gecko/models/g1_wallets_list.dart'; import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/generate_wallets.dart'; +import 'package:gecko/providers/settings_provider.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/providers/wallets_profiles.dart'; import 'package:gecko/providers/home.dart'; @@ -59,12 +60,16 @@ Future main() async { } HomeProvider _homeProvider = HomeProvider(); - DuniterIndexer _duniterIndexer = DuniterIndexer(); + // DuniterIndexer _duniterIndexer = DuniterIndexer(); await initHiveForFlutter(); await _homeProvider.initHive(); appVersion = await _homeProvider.getAppVersion(); prefs = await SharedPreferences.getInstance(); + // Reset GraphQL cache + final cache = HiveStore(); + cache.reset(); + // Configure Hive and open boxes Hive.registerAdapter(WalletDataAdapter()); Hive.registerAdapter(ChestDataAdapter()); @@ -84,8 +89,6 @@ Future main() async { } // log.d(await configBox.get('endpoint')); - _duniterIndexer.getValidIndexerEndpoint(); - HttpOverrides.global = MyHttpOverrides(); if (kReleaseMode && enableSentry) { @@ -148,7 +151,8 @@ class Gecko extends StatelessWidget { ChangeNotifierProvider(create: (_) => SearchProvider()), ChangeNotifierProvider(create: (_) => CesiumPlusProvider()), ChangeNotifierProvider(create: (_) => SubstrateSdk()), - ChangeNotifierProvider(create: (_) => DuniterIndexer()) + ChangeNotifierProvider(create: (_) => DuniterIndexer()), + ChangeNotifierProvider(create: (_) => SettingsProvider()) ], child: MaterialApp( localizationsDelegates: context.localizationDelegates, diff --git a/lib/providers/duniter_indexer.dart b/lib/providers/duniter_indexer.dart index 26c8c5f..b3d079c 100644 --- a/lib/providers/duniter_indexer.dart +++ b/lib/providers/duniter_indexer.dart @@ -22,41 +22,83 @@ class DuniterIndexer with ChangeNotifier { int nPage = 1; int nRepositories = 20; List? transBC; + List listIndexerEndpoints = []; + bool isLoadingIndexer = false; void reload() { notifyListeners(); } - Future checkIndexerEndpoint() async { - final oldEndpoint = indexerEndpoint; - while (true) { - await Future.delayed(const Duration(seconds: 30)); - final _client = HttpClient(); - _client.connectionTimeout = const Duration(milliseconds: 4000); - try { - final request = - await _client.postUrl(Uri.parse('$oldEndpoint/v1/graphql')); - final response = await request.close(); - if (response.statusCode != 200) { - log.d('INDEXER IS OFFILINE'); - indexerEndpoint = ''; - } else { - // log.d('Indexer is online'); - indexerEndpoint = oldEndpoint; - } - } catch (e) { + Future checkIndexerEndpoint(String endpoint) async { + isLoadingIndexer = true; + notifyListeners(); + final _client = HttpClient(); + _client.connectionTimeout = const Duration(milliseconds: 4000); + try { + final request = await _client.postUrl(Uri.parse('$endpoint/v1/graphql')); + final response = await request.close(); + if (response.statusCode != 200) { log.d('INDEXER IS OFFILINE'); indexerEndpoint = ''; + isLoadingIndexer = false; + notifyListeners(); + return false; + } else { + indexerEndpoint = endpoint; + await configBox.put('indexerEndpoint', endpoint); + // await configBox.put('customEndpoint', endpoint); + isLoadingIndexer = false; + notifyListeners(); + final cache = HiveStore(); + cache.reset(); + return true; } + } catch (e) { + log.d('INDEXER IS OFFILINE'); + indexerEndpoint = ''; + isLoadingIndexer = false; + notifyListeners(); + return false; } } + // Future checkIndexerEndpointBackground() async { + // final oldEndpoint = indexerEndpoint; + // while (true) { + // await Future.delayed(const Duration(seconds: 30)); + // final isValid = await checkIndexerEndpoint(oldEndpoint); + // if (!isValid) { + // log.d('INDEXER IS OFFILINE'); + // indexerEndpoint = ''; + // } else { + // // log.d('Indexer is online'); + // indexerEndpoint = oldEndpoint; + // } + // } + // } + Future getValidIndexerEndpoint() async { - List _listEndpoints = await rootBundle + // await configBox.delete('indexerEndpoint'); + + listIndexerEndpoints = await rootBundle .loadString('config/indexer_endpoints.json') .then((jsonStr) => jsonDecode(jsonStr)); // _listEndpoints.shuffle(); + log.d(listIndexerEndpoints); + listIndexerEndpoints.add('Personnalisé'); + + if (configBox.containsKey('customIndexer')) { + return configBox.get('customIndexer'); + // listIndexerEndpoints.insert(0, configBox.get('customIndexer')); + } + + if (configBox.containsKey('indexerEndpoint')) { + if (await checkIndexerEndpoint(configBox.get('indexerEndpoint'))) { + return configBox.get('indexerEndpoint'); + } + } + int i = 0; // String _endpoint = ''; int _statusCode = 0; @@ -65,25 +107,28 @@ class DuniterIndexer with ChangeNotifier { _client.connectionTimeout = const Duration(milliseconds: 3000); do { - int listLenght = _listEndpoints.length; + int listLenght = listIndexerEndpoints.length; if (i >= listLenght) { log.e('NO VALID INDEXER ENDPOINT FOUND'); indexerEndpoint = ''; break; } - log.d( - (i + 1).toString() + 'n indexer endpoint try: ${_listEndpoints[i]}'); + log.d((i + 1).toString() + + 'n indexer endpoint try: ${listIndexerEndpoints[i]}'); if (i != 0) { await Future.delayed(const Duration(milliseconds: 300)); } try { - final request = - await _client.postUrl(Uri.parse('${_listEndpoints[i]}/v1/graphql')); + String endpointPath = '${listIndexerEndpoints[i]}/v1/graphql'; + + final request = await _client.postUrl(Uri.parse(endpointPath)); final response = await request.close(); - indexerEndpoint = _listEndpoints[i]; + indexerEndpoint = listIndexerEndpoints[i]; + await configBox.put('indexerEndpoint', listIndexerEndpoints[i]); + _statusCode = response.statusCode; i++; } on TimeoutException catch (_) { @@ -202,13 +247,15 @@ class DuniterIndexer with ChangeNotifier { return const Text('Aucun résultat'); } + log.d(indexerEndpoint); final _httpLink = HttpLink( '$indexerEndpoint/v1/graphql', ); final _client = ValueNotifier( GraphQLClient( - cache: GraphQLCache(store: HiveStore()), + cache: GraphQLCache( + store: HiveStore()), // GraphQLCache(store: HiveStore()) link: _httpLink, ), ); diff --git a/lib/providers/home.dart b/lib/providers/home.dart index f55af59..f957be1 100644 --- a/lib/providers/home.dart +++ b/lib/providers/home.dart @@ -83,6 +83,9 @@ class HomeProvider with ChangeNotifier { Future getValidEndpoints() async { await configBox.delete('endpoint'); + if (!configBox.containsKey('autoEndpoint')) { + configBox.put('autoEndpoint', true); + } List _listEndpoints = []; if (!configBox.containsKey('endpoint') || diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart new file mode 100644 index 0000000..9db6eb7 --- /dev/null +++ b/lib/providers/settings_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +class SettingsProvider with ChangeNotifier { + void reload() { + notifyListeners(); + } +} diff --git a/lib/providers/substrate_sdk.dart b/lib/providers/substrate_sdk.dart index 5de4a37..46abbf2 100644 --- a/lib/providers/substrate_sdk.dart +++ b/lib/providers/substrate_sdk.dart @@ -46,7 +46,6 @@ class SubstrateSdk with ChangeNotifier { } Future connectNode(BuildContext ctx) async { - List node = []; HomeProvider _homeProvider = Provider.of(ctx, listen: false); // var connectivityResult = await (Connectivity().checkConnectivity()); @@ -57,13 +56,14 @@ class SubstrateSdk with ChangeNotifier { // } _homeProvider.changeMessage("connectionPending".tr(), 0); - for (String _endpoint in configBox.get('endpoint')) { - final n = NetworkParams(); - n.name = currencyName; - n.endpoint = _endpoint; - n.ss58 = ss58; - node.add(n); - } + // configBox.delete('customEndpoint'); + final List listEndpoints = + configBox.containsKey('customEndpoint') + ? [getDuniterCustomEndpoint()] + : getDuniterBootstrap(); + + // final nodes = getDuniterBootstrap(); + int timeout = 10000; // if (n.endpoint!.startsWith('ws://')) { @@ -93,7 +93,7 @@ class SubstrateSdk with ChangeNotifier { isLoadingEndpoint = true; notifyListeners(); - final res = await sdk.api.connectNode(keyring, node).timeout( + final res = await sdk.api.connectNode(keyring, listEndpoints).timeout( Duration(milliseconds: timeout), onTimeout: () => null, ); @@ -133,6 +133,27 @@ class SubstrateSdk with ChangeNotifier { log.d(sdk.api.connectedNode?.endpoint); } + List getDuniterBootstrap() { + List node = []; + + for (String _endpoint in configBox.get('endpoint')) { + final n = NetworkParams(); + n.name = currencyName; + n.endpoint = _endpoint; + n.ss58 = ss58; + node.add(n); + } + return node; + } + + NetworkParams getDuniterCustomEndpoint() { + final nodeParams = NetworkParams(); + nodeParams.name = currencyName; + nodeParams.endpoint = configBox.get('customEndpoint'); + nodeParams.ss58 = ss58; + return nodeParams; + } + Future importAccount( {String mnemonic = '', bool fromMnemonic = false, diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 58468b3..26c75f4 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -107,6 +107,10 @@ class HomeScreen extends StatelessWidget { builder: (ctx) => StatefulWrapper( onInit: () { WidgetsBinding.instance.addPostFrameCallback((_) async { + DuniterIndexer _duniterIndexer = + Provider.of(ctx, listen: false); + _duniterIndexer.getValidIndexerEndpoint(); + if (!_sub.sdkReady && !_sub.sdkLoading) await _sub.initApi(); if (_sub.sdkReady && !_sub.nodeConnected) { // Check if versionData non compatible, drop everything @@ -147,9 +151,7 @@ class HomeScreen extends StatelessWidget { } }); } - DuniterIndexer _duniterIndexer = - Provider.of(ctx, listen: false); - _duniterIndexer.checkIndexerEndpoint(); + // _duniterIndexer.checkIndexerEndpointBackground(); }); }, child: isWalletsExists ? geckHome(context) : welcomeHome(context) diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index bc0b488..09e0e3d 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -1,26 +1,17 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:durt/durt.dart'; import 'package:flutter/services.dart'; +import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/my_wallets.dart'; +import 'package:gecko/providers/settings_provider.dart'; import 'package:gecko/providers/substrate_sdk.dart'; -import 'dart:io'; import 'package:gecko/globals.dart'; +import 'package:polkawallet_sdk/api/types/networkParams.dart'; import 'package:provider/provider.dart'; +// import 'package:dropdown_button2/dropdown_button2.dart'; // ignore: must_be_immutable class SettingsScreen extends StatelessWidget { - String? generatedMnemonic; - bool walletIsGenerated = false; - NewWallet? actualWallet; - String? newWalletName; - - bool hasError = false; - String validPin = 'NO PIN'; - String currentText = ""; - var pinColor = Colors.grey[300]; - Directory? appPath; - final MyWalletsProvider _myWallets = MyWalletsProvider(); SettingsScreen({Key? key}) : super(key: key); @@ -32,10 +23,7 @@ class SettingsScreen extends StatelessWidget { const double buttonHigh = 50; const double buttonWidth = 240; const double fontSize = 16; - TextEditingController _endpointController = - TextEditingController(text: configBox.get('endpoint').first); - // getAppDirectory(); return Scaffold( backgroundColor: backgroundColor, appBar: AppBar( @@ -44,75 +32,303 @@ class SettingsScreen extends StatelessWidget { height: 22, child: Text('parameters'.tr()), )), - body: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 60), - Row(children: [ - Consumer(builder: (context, _sub, _) { - log.d(_sub.sdk.api.connectedNode?.endpoint); - return Expanded( - child: Row(children: [ - const SizedBox(width: 10), - Text('currencyNode'.tr(args: [currencyName])), - const Spacer(), - Icon(_sub.nodeConnected && !_sub.isLoadingEndpoint - ? Icons.check - : Icons.close), - const Spacer(), - SizedBox( - width: 200, - height: 50, - child: TextField( - controller: _endpointController, - autocorrect: false, - ), - ), - const Spacer(flex: 5), - _sub.isLoadingEndpoint - ? CircularProgressIndicator(color: orangeC) - : IconButton( - icon: Icon( - Icons.send, - color: orangeC, - size: 40, - ), - onPressed: () async { - configBox - .put('endpoint', [_endpointController.text]); - await _sub.connectNode(context); - }), - const Spacer(flex: 8), - ]), - ); - }), - ]), - // SizedBox(height: isTall ? 80 : 120), - const Spacer(), - SizedBox( - height: buttonHigh, - width: buttonWidth, - child: Center( - child: InkWell( - key: const Key('deleteChest'), - onTap: () async { - log.i('Oublier tous mes coffres'); - await _myWallets.deleteAllWallet(context); - }, - child: Text( - 'forgetAllMyChests'.tr(), - style: const TextStyle( - fontSize: fontSize + 4, - color: Color(0xffD80000), - fontWeight: FontWeight.w600, - ), - ), + body: Column(children: [ + const SizedBox(height: 30), + duniterEndpointSelection(context), + const SizedBox(height: 50), + indexerEndpointSelection(context), + + // SizedBox(height: isTall ? 80 : 120), + const Spacer(), + SizedBox( + height: buttonHigh, + width: buttonWidth, + child: Center( + child: InkWell( + key: const Key('deleteChest'), + onTap: () async { + log.i('Oublier tous mes coffres'); + await _myWallets.deleteAllWallet(context); + }, + child: Text( + 'forgetAllMyChests'.tr(), + style: const TextStyle( + fontSize: fontSize + 4, + color: Color(0xffD80000), + fontWeight: FontWeight.w600, ), ), ), - // const Spacer(), - SizedBox(height: isTall ? 90 : 60), - ]), + ), + ), + // const Spacer(), + SizedBox(height: isTall ? 90 : 60), + ]), ); } + + Widget duniterEndpointSelection(BuildContext context) { + SubstrateSdk _sub = Provider.of(context, listen: false); + String? selectedDuniterEndpoint; + + // List of items in our dropdown menu + var duniterBootstrapNodes = _sub.getDuniterBootstrap(); + selectedDuniterEndpoint = + _sub.getConnectedEndpoint() ?? duniterBootstrapNodes.first.endpoint; + + final customEndpoint = NetworkParams(); + customEndpoint.name = currencyName; + customEndpoint.endpoint = 'Personnalisé'; + customEndpoint.ss58 = ss58; + + final automaticEndpoint = NetworkParams(); + automaticEndpoint.name = currencyName; + automaticEndpoint.endpoint = 'Auto'; + automaticEndpoint.ss58 = ss58; + // duniterBootstrapNodes.add(_sub.getDuniterCustomEndpoint()); + duniterBootstrapNodes.insert(0, automaticEndpoint); + duniterBootstrapNodes.add(customEndpoint); + + if (configBox.get('autoEndpoint') == true) { + selectedDuniterEndpoint = automaticEndpoint.endpoint; + } else if (configBox.containsKey('customEndpoint')) { + selectedDuniterEndpoint = customEndpoint.endpoint; + } + + TextEditingController _endpointController = TextEditingController( + text: configBox.containsKey('customEndpoint') + ? configBox.get('customEndpoint') + : 'wss://'); + + return Column(children: [ + Row(children: [ + Consumer(builder: (context, _sub, _) { + log.d(_sub.sdk.api.connectedNode?.endpoint); + return Expanded( + child: Row(children: [ + const SizedBox(width: 10), + Text('currencyNode'.tr(args: [currencyName])), + const Spacer(), + Icon(_sub.nodeConnected && !_sub.isLoadingEndpoint + ? Icons.check + : Icons.close), + const Spacer(), + Consumer(builder: (context, _set, _) { + return DropdownButtonHideUnderline( + child: DropdownButton( + // alignment: AlignmentDirectional.topStart, + value: selectedDuniterEndpoint, + icon: const Icon(Icons.keyboard_arrow_down), + items: duniterBootstrapNodes + .map((NetworkParams _endpointParams) { + return DropdownMenuItem( + value: _endpointParams.endpoint, + child: Text(_endpointParams.endpoint!), + ); + }).toList(), + onChanged: (String? _newEndpoint) { + log.d(_newEndpoint!); + selectedDuniterEndpoint = _newEndpoint; + _set.reload(); + }, + ), + ); + }), + const Spacer(flex: 5), + _sub.isLoadingEndpoint + ? CircularProgressIndicator(color: orangeC) + : Consumer(builder: (context, _set, _) { + return IconButton( + icon: Icon( + Icons.send, + color: selectedDuniterEndpoint != + _sub.getConnectedEndpoint() + ? orangeC + : Colors.grey[500], + size: 40, + ), + onPressed: selectedDuniterEndpoint != + _sub.getConnectedEndpoint() + ? () async { + if (selectedDuniterEndpoint == 'Auto') { + configBox.delete('customEndpoint'); + configBox.put('autoEndpoint', true); + } else { + configBox.put('autoEndpoint', false); + final finalEndpoint = + selectedDuniterEndpoint == + 'Personnalisé' + ? _endpointController.text + : selectedDuniterEndpoint; + configBox.put( + 'customEndpoint', finalEndpoint); + } + await _sub.connectNode(context); + } + : null); + }), + const Spacer(flex: 8), + ]), + ); + }), + ]), + Consumer(builder: (context, _set, _) { + return Visibility( + visible: selectedDuniterEndpoint == 'Personnalisé', + child: SizedBox( + width: 200, + height: 50, + child: TextField( + controller: _endpointController, + autocorrect: false, + ), + ), + ); + }), + Consumer(builder: (context, _sub, _) { + return Consumer(builder: (context, _set, _) { + return Visibility( + visible: selectedDuniterEndpoint == 'Auto', + child: SizedBox( + width: 250, + height: _sub.getConnectedEndpoint() == null ? 60 : 20, + child: Text( + _sub.getConnectedEndpoint() ?? + "Un noeud sûr et valide sera choisi automatiquement parmis une liste aléatoire.", + style: TextStyle( + fontSize: 15, + fontStyle: FontStyle.italic, + color: Colors.grey[700]), + ), + ), + ); + }); + }), + ]); + } + + Widget indexerEndpointSelection(BuildContext context) { + DuniterIndexer _indexer = + Provider.of(context, listen: false); + + String? selectedIndexerEndpoint; + if (configBox.containsKey('customIndexer')) { + selectedIndexerEndpoint = 'Personnalisé'; + } else { + selectedIndexerEndpoint = indexerEndpoint; + } + + if (selectedIndexerEndpoint == '') { + selectedIndexerEndpoint = _indexer.listIndexerEndpoints[0]; + } + + TextEditingController _indexerEndpointController = TextEditingController( + text: configBox.containsKey('customIndexer') + ? configBox.get('customIndexer') + : 'https://'); + + return Column(children: [ + Row(children: [ + Consumer(builder: (context, _indexer, _) { + log.d(selectedIndexerEndpoint); + log.d(_indexer.listIndexerEndpoints); + return Expanded( + child: Row(children: [ + const SizedBox(width: 10), + const Text('Indexer : '), + const Spacer(), + Icon(indexerEndpoint != '' ? Icons.check : Icons.close), + const Spacer(), + Consumer(builder: (context, _set, _) { + return DropdownButtonHideUnderline( + child: DropdownButton( + // alignment: AlignmentDirectional.topStart, + value: selectedIndexerEndpoint, + icon: const Icon(Icons.keyboard_arrow_down), + items: + _indexer.listIndexerEndpoints.map((_indexerEndpoint) { + return DropdownMenuItem( + value: _indexerEndpoint, + child: Text(_indexerEndpoint), + ); + }).toList(), + onChanged: (_newEndpoint) { + log.d(_newEndpoint!); + selectedIndexerEndpoint = _newEndpoint.toString(); + _set.reload(); + }, + ), + ); + }), + const Spacer(flex: 5), + _indexer.isLoadingIndexer + ? CircularProgressIndicator(color: orangeC) + : Consumer(builder: (context, _set, _) { + return IconButton( + icon: Icon( + Icons.send, + color: selectedIndexerEndpoint != indexerEndpoint + ? orangeC + : Colors.grey[500], + size: 40, + ), + onPressed: selectedIndexerEndpoint != indexerEndpoint + ? () async { + final finalEndpoint = + selectedIndexerEndpoint == 'Personnalisé' + ? _indexerEndpointController.text + : selectedIndexerEndpoint!; + + if (selectedIndexerEndpoint == + 'Personnalisé') { + configBox.put('customIndexer', + _indexerEndpointController.text); + } else { + configBox.delete('customIndexer'); + } + log.d('connection to indexer $finalEndpoint'); + await _indexer + .checkIndexerEndpoint(finalEndpoint); + } + : null); + }), + const Spacer(flex: 8), + ]), + ); + }), + ]), + Consumer(builder: (context, _set, _) { + return Visibility( + visible: selectedIndexerEndpoint == 'Personnalisé', + child: SizedBox( + width: 200, + height: 50, + child: TextField( + controller: _indexerEndpointController, + autocorrect: false, + ), + ), + ); + }), + Consumer(builder: (context, _sub, _) { + return Consumer(builder: (context, _set, _) { + return Visibility( + visible: selectedIndexerEndpoint == 'Auto', + child: SizedBox( + width: 250, + height: 60, + child: Text( + _sub.getConnectedEndpoint() ?? + "Un noeud sûr et valide sera choisi automatiquement parmis une liste aléatoire.", + style: TextStyle( + fontSize: 15, + fontStyle: FontStyle.italic, + color: Colors.grey[700]), + ), + ), + ); + }); + }), + ]); + } } diff --git a/pubspec.lock b/pubspec.lock index a8e8ef7..6eb5e7f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -309,6 +309,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + dropdown_button2: + dependency: "direct main" + description: + name: dropdown_button2 + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.3" durt: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c926352..6a18ef7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: Pay with G1. # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 0.0.9+14 +version: 0.0.9+15 environment: sdk: '>=2.12.0 <3.0.0' @@ -74,6 +74,7 @@ dependencies: image_cropper: ^2.0.3 easy_localization: ^3.0.1 flutter_markdown: ^0.6.10+2 + dropdown_button2: ^1.6.3 dev_dependencies: # flutter_launcher_icons: ^0.9.2