diff --git a/lib/data/models/transaction_cubit.dart b/lib/data/models/transaction_cubit.dart index 7d0405cf5e4fd85cc5d444016cb5ab0622b54f85..4222bbb1a5e5ed716b2f3c137b2d402f9562122b 100644 --- a/lib/data/models/transaction_cubit.dart +++ b/lib/data/models/transaction_cubit.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:tuple/tuple.dart'; import '../../../g1/api.dart'; import '../../../g1/transaction_parser.dart'; @@ -8,7 +9,9 @@ import '../../ui/contacts_cache.dart'; import '../../ui/logger.dart'; import '../../ui/notification_controller.dart'; import 'contact.dart'; +import 'node.dart'; import 'node_list_cubit.dart'; +import 'node_type.dart'; import 'transaction.dart'; import 'transaction_balance_state.dart'; import 'transaction_type.dart'; @@ -38,46 +41,67 @@ class TransactionsCubit extends HydratedCubit<TransactionsAndBalanceState> { emit(state.copyWith(transactions: newTransactions, balance: newBalance)); } - Future<void> fetchTransactions(NodeListCubit cubit) async { - logger('Loading transactions --------------------'); - final Map<String, dynamic>? txData = - await gvaHistoryAndBalance(SharedPreferencesHelper().getPubKey()); - if (txData == null) { - logger('Failed to get transactions'); - return; - } - final TransactionsAndBalanceState newState = - transactionsGvaParser(txData, state); - // Notify - //logger( - // 'Last received: ${lastReceived.toIso8601String()}, last received notification: ${lastReceivedNotification.toIso8601String()}, compared ${lastReceived.compareTo(lastReceivedNotification)}'); - logger( - 'Last received notification: ${newState.latestReceivedNotification.toIso8601String()})}'); - logger( - 'Last sent notification: ${newState.latestSentNotification.toIso8601String()})}'); - emit(newState); - for (final Transaction tx in newState.transactions.reversed) { - if (tx.type == TransactionType.received && - newState.latestReceivedNotification.isBefore(tx.time)) { - // Future - final Contact from = await ContactsCache().getContact(tx.from); - NotificationController.createNewNotification( - tx.time.millisecondsSinceEpoch.toString(), - amount: tx.amount / 100, - from: from.title); - emit(newState.copyWith(latestReceivedNotification: tx.time)); + Future<void> fetchTransactions(NodeListCubit cubit, {int retries = 5}) async { + Tuple2<Map<String, dynamic>?, Node> txDataResult; + bool success = false; + + for (int attempt = 0; attempt < retries; attempt++) { + txDataResult = + await gvaHistoryAndBalance(SharedPreferencesHelper().getPubKey()); + final Node node = txDataResult.item2; + logger('Loading transactions using $node --------------------'); + + if (txDataResult.item1 == null) { + logger( + 'Failed to get transactions, attempt ${attempt + 1} of $retries'); + await Future<void>.delayed(const Duration(seconds: 1)); + increaseNodeErrors(NodeType.gva, node); + continue; + } + + final Map<String, dynamic> txData = txDataResult.item1!; + final TransactionsAndBalanceState newState = + transactionsGvaParser(txData, state); + + if (newState.balance < 0) { + logger('Warning: Negative balance in node ${txDataResult.item2}'); + increaseNodeErrors(NodeType.gva, node); + continue; } - if (tx.type == TransactionType.sent && - newState.latestSentNotification.isBefore(tx.time)) { - // Future - final Contact to = await ContactsCache().getContact(tx.from); - NotificationController.createNewNotification( - tx.time.millisecondsSinceEpoch.toString(), - amount: -tx.amount / 100, - to: to.title); - emit(newState.copyWith(latestSentNotification: tx.time)); + success = true; + + logger( + 'Last received notification: ${newState.latestReceivedNotification.toIso8601String()})}'); + logger( + 'Last sent notification: ${newState.latestSentNotification.toIso8601String()})}'); + emit(newState); + for (final Transaction tx in newState.transactions.reversed) { + if (tx.type == TransactionType.received && + newState.latestReceivedNotification.isBefore(tx.time)) { + // Future + final Contact from = await ContactsCache().getContact(tx.from); + NotificationController.createNewNotification( + tx.time.millisecondsSinceEpoch.toString(), + amount: tx.amount / 100, + from: from.title); + emit(newState.copyWith(latestReceivedNotification: tx.time)); + } + if (tx.type == TransactionType.sent && + newState.latestSentNotification.isBefore(tx.time)) { + // Future + final Contact to = await ContactsCache().getContact(tx.from); + NotificationController.createNewNotification( + tx.time.millisecondsSinceEpoch.toString(), + amount: -tx.amount / 100, + to: to.title); + emit(newState.copyWith(latestSentNotification: tx.time)); + } } } + if (!success) { + logger('Failed to get transactions after $retries attempts'); + return; + } } @override diff --git a/lib/g1/api.dart b/lib/g1/api.dart index c4ec52b32d9230df3ce68fbd5d8fc256fafb5516..1f09ddc492e6ad79288bac46e5bd64cc1b72a858 100644 --- a/lib/g1/api.dart +++ b/lib/g1/api.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:tuple/tuple.dart'; import 'package:universal_html/html.dart' show window; import '../data/models/contact.dart'; @@ -496,8 +497,7 @@ Future<http.Response> _requestWithRetry( } catch (e) { logger('Error trying ${node.url} $e'); if (!dontRecord) { - logger('Increasing node errors of ${node.url} (${node.errors})'); - NodeManager().updateNode(type, node.copyWith(errors: node.errors + 1)); + increaseNodeErrors(type, node); } continue; } @@ -574,37 +574,36 @@ String proxyfyNode(String nodeUrl) { return url; } -Future<Map<String, dynamic>?> gvaHistoryAndBalance(String pubKey) async { +Future<Tuple2<Map<String, dynamic>?, Node>> gvaHistoryAndBalance( + String pubKey) async { return gvaFunctionWrapper<Map<String, dynamic>>( pubKey, (Gva gva) => gva.history(pubKey)); } -Future<double?> gvaBalance(String pubKey) async { +Future<Tuple2<double?, Node>> gvaBalance(String pubKey) async { return gvaFunctionWrapper<double>(pubKey, (Gva gva) => gva.balance(pubKey)); } -Future<String?> gvaNick(String pubKey) async { +Future<Tuple2<String?, Node>> gvaNick(String pubKey) async { return gvaFunctionWrapper<String>( pubKey, (Gva gva) => gva.getUsername(pubKey)); } -Future<T?> gvaFunctionWrapper<T>( +Future<Tuple2<T?, Node>> gvaFunctionWrapper<T>( String pubKey, Future<T?> Function(Gva) specificFunction) async { final List<Node> nodes = _getBestGvaNodes(); for (int i = 0; i < nodes.length; i++) { final Node node = nodes[i]; try { final Gva gva = Gva(node: proxyfyNode(node.url)); - logger('Trying use gva ${node.url}'); + logger('Trying to use gva ${node.url}'); final T? result = await specificFunction(gva); - return result; + return Tuple2<T?, Node>(result, node); } catch (e) { // await Sentry.captureMessage( // 'Error trying to use gva node ${node.url} $e'); logger('Error trying ${node.url} $e'); - logger('Increasing node errors of ${node.url} (${node.errors})'); - NodeManager() - .updateNode(NodeType.gva, node.copyWith(errors: node.errors + 1)); + increaseNodeErrors(NodeType.gva, node); continue; } } @@ -638,3 +637,8 @@ class NodeCheck { final Duration latency; final int currentBlock; } + +void increaseNodeErrors(NodeType type, Node node) { + logger('Increasing node errors of ${node.url} (${node.errors})'); + NodeManager().updateNode(type, node.copyWith(errors: node.errors + 1)); +} diff --git a/lib/g1/transaction_parser.dart b/lib/g1/transaction_parser.dart index 556570c41c75ceb04b5eade2a1237b1f7ca6c88d..d5c5ab9747a95d799509e5eb828fc5e236378625 100644 --- a/lib/g1/transaction_parser.dart +++ b/lib/g1/transaction_parser.dart @@ -57,13 +57,9 @@ TransactionsAndBalanceState transactionsGvaParser( Map<String, dynamic> txData, TransactionsAndBalanceState state) { // Balance final dynamic rawBalance = txData['balance']; - final double? amount; - if (rawBalance != null) { - final Map<String, dynamic> balance = rawBalance as Map<String, dynamic>; - amount = (balance['amount'] as int) / 1.0; - } else { - amount = 0; - } + final double amount = rawBalance != null + ? ((rawBalance as Map<String, dynamic>)['amount'] as int) / 1.0 + : 0; // Transactions final Map<String, dynamic> txsHistoryBc = txData['txsHistoryBc'] as Map<String, dynamic>; diff --git a/lib/ui/widgets/fourth_screen/transaction_page.dart b/lib/ui/widgets/fourth_screen/transaction_page.dart index da9ff95d21372d1e4e319016ef74aeee445635b9..87c711ef2a1d143267a0557fe176d1987d870949 100644 --- a/lib/ui/widgets/fourth_screen/transaction_page.dart +++ b/lib/ui/widgets/fourth_screen/transaction_page.dart @@ -34,7 +34,7 @@ class _TransactionsAndBalanceWidgetState void initState() { super.initState(); _transScrollController.addListener(_scrollListener); -// Remove in the future + // Remove in the future transCubit = context.read<TransactionsCubit>(); nodeListCubit = context.read<NodeListCubit>(); transCubit.fetchTransactions(nodeListCubit); diff --git a/pubspec.lock b/pubspec.lock index f0a252e135d1c42cebcbe9911f6a285eeb392b6b..696e5e73ada37cf3b3e66a7bc370e345d3452d72 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1337,6 +1337,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tuple: + dependency: "direct main" + description: + name: tuple + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" + source: hosted + version: "2.0.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9858c10edf745360c0db0df05de92b0b83962cb8..2b9baaec7883fd65b03c176876556bcc13ca0853 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,7 @@ dependencies: barcode_scan2: ^4.2.4 jsqr: ^0.1.4 web_browser_detect: ^2.0.3 + tuple: ^2.0.1 dev_dependencies: flutter_test: