Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • DanBaDo/ginkgo
  • flodef/ginkgo
  • zicmama/ginkgo
  • vjrj/ginkgo
  • pokapow/ginkgo
  • weblate/ginkgo
6 results
Show changes
Showing
with 2613 additions and 821 deletions
enum NodeType { duniter, cesiumPlus, gva, endpoint, duniterIndexer }
enum NodeType {
duniter,
cesiumPlus,
gva,
endpoint,
duniterIndexer,
datapodEndpoint,
ipfsGateway
}
import 'package:flutter/foundation.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../ui/ui_helpers.dart';
import '../../ui/in_dev_helper.dart';
import 'contact.dart';
import 'payment_state.dart';
......@@ -11,6 +11,8 @@ class PaymentCubit extends HydratedCubit<PaymentState> {
@override
String get storagePrefix => kIsWeb ? 'PaymentCubit' : super.storagePrefix;
List<Contact> get contacts => state.contacts;
void updatePayment({
String? description,
double? amount,
......
......@@ -73,7 +73,7 @@ class Transaction extends Equatable {
amount,
comment,
time,
debugInfo,
// debugInfo,
recipients,
recipientsAmounts
];
......
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:collection';
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';
import '../../g1/currency.dart';
import '../../g1/g1_helper.dart';
import '../../shared_prefs_helper.dart';
import '../../ui/logger.dart';
import '../../ui/notification_controller.dart';
import '../../ui/pay_helper.dart';
import 'app_cubit.dart';
import 'contact.dart';
import 'node.dart';
import 'node_list_cubit.dart';
import 'node_type.dart';
import 'transaction.dart';
import 'transaction_state.dart';
import 'transaction_type.dart';
class TransactionCubitRemove extends HydratedCubit<TransactionState> {
TransactionCubitRemove()
: super(TransactionState(
transactions: const <Transaction>[],
pendingTransactions: const <Transaction>[],
balance: 0,
lastChecked: DateTime.now()));
@override
String get storagePrefix =>
kIsWeb ? 'TransactionsCubit' : super.storagePrefix;
@Deprecated('Use MultiWalletTransactionCubit instead.')
void addPendingTransaction(Transaction pendingTransaction) {
final TransactionState currentState = state;
final List<Transaction> newPendingTransactions =
List<Transaction>.of(currentState.pendingTransactions)
..add(pendingTransaction);
emit(currentState.copyWith(pendingTransactions: newPendingTransactions));
}
void removePendingTransaction(Transaction pendingTransaction) {
final TransactionState currentState = state;
final List<Transaction> newPendingTransactions =
List<Transaction>.of(currentState.pendingTransactions)
..remove(pendingTransaction);
emit(currentState.copyWith(pendingTransactions: newPendingTransactions));
}
Future<List<Transaction>> fetchTransactions(
NodeListCubit cubit, AppCubit appCubit,
{int retries = 5, int? pageSize, String? cursor}) async {
Tuple2<Map<String, dynamic>?, Node> txDataResult;
bool success = false;
final bool isG1 = appCubit.currency == Currency.G1;
for (int attempt = 0; attempt < retries; attempt++) {
final String myPubKey = SharedPreferencesHelper().getPubKey();
txDataResult = await gvaHistoryAndBalance(myPubKey, pageSize, cursor);
final Node node = txDataResult.item2;
logger(
'Loading transactions using $node (pageSize: $pageSize, cursor: $cursor) --------------------');
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!;
TransactionState newState =
await transactionsGvaParser(txData, state, myPubKey);
if (newState.balance < 0) {
logger('Warning: Negative balance in node ${txDataResult.item2}');
increaseNodeErrors(NodeType.gva, node);
continue;
}
success = true;
if (newState.currentUd != null) {
appCubit.setUd(newState.currentUd!);
}
logger(
'Last received notification: ${newState.latestReceivedNotification.toIso8601String()})}');
logger(
'Last sent notification: ${newState.latestSentNotification.toIso8601String()})}');
// Check pending transactions
if (cursor == null) {
// First page, so let's check pending transactions
final LinkedHashSet<Transaction> newPendingTransactions =
LinkedHashSet<Transaction>();
final List<Transaction> newTransactions = <Transaction>[];
// Index transactions by key
final Map<String, Transaction> txMap = <String, Transaction>{};
final Map<String, Transaction> pendingMap = <String, Transaction>{};
// or maybe it doesn't merit the effort
for (final Transaction t in newState.transactions) {
txMap[_getTxKey(t)] = t;
}
// Get a range of tx in 1h
for (final Transaction t in newState.transactions) {
txMap[_getTxKey(t)] = t;
}
for (final Transaction t in newState.pendingTransactions) {
pendingMap[_getTxKey(t)] = t;
}
// Adjust pending transactions
for (final Transaction pend in newState.pendingTransactions) {
if (pend.type == TransactionType.waitingNetwork) {
newPendingTransactions.add(pend);
continue;
}
if (txMap[_getTxKey(pend)] != null) {
// Found a match
// VER SI SENT o que
final Transaction t = txMap[_getTxKey(pend)]!;
if (t.type == TransactionType.sent) {
loggerDev(
'@@@@@ Found a sent match for pending transaction ${pend.toStringSmall(myPubKey)}');
// Add later the tx, but don't add the pending
} else {
if (t.type == TransactionType.sending) {
loggerDev(
'@@@@@ Found a sending match for pending transaction ${pend.toStringSmall(myPubKey)}');
// Re-add as pending
// The tx will not be add as sending (as some nodes will show it and others will not,
// we use better the pending)
// FIXME: if this is old, probably is stuck, so maybe we should cancel->retry
newPendingTransactions.add(pend.copyWith(
debugInfo:
pend.debugInfo ?? 'Node where see it: ${node.url}'));
} else {
loggerDev(
'@@@@@ WARNING: Found a ${t.type} match for pending transaction ${pend.toStringSmall(myPubKey)}');
}
}
} else {
// Not found a match
if (areDatesClose(DateTime.now(), pend.time, paymentTimeRange)) {
loggerDev(
'@@@@@ Not found yet pending transaction ${pend.toStringSmall(myPubKey)}');
newPendingTransactions.add(pend);
} else {
// Old pending transaction, warn user
loggerDev(
'@@@@@ Warn user: Not found an old pending transaction ${pend.toStringSmall(myPubKey)}');
// Add it but with missing type
newPendingTransactions
.add(pend.copyWith(type: TransactionType.failed));
}
}
}
for (final Transaction tx in newState.transactions) {
if (pendingMap[_getTxKey(tx)] != null &&
(tx.type == TransactionType.sending ||
tx.type == TransactionType.sent)) {
// Found a match
if (tx.type == TransactionType.sent) {
// Ok add it, but not as pending
newTransactions.add(tx);
} else {
// It's sending so should be added before as pending
}
} else {
// Does not match
if (tx.type == TransactionType.sending) {
// Not found, maybe we are in other client, so add as pending
newPendingTransactions
.add(tx.copyWith(type: TransactionType.pending));
} else {
// the rest
newTransactions.add(tx);
}
}
}
newState = newState.copyWith(
transactions: newTransactions,
pendingTransactions: newPendingTransactions.toList());
}
emit(newState);
for (final Transaction tx in newState.transactions.reversed) {
if (tx.type == TransactionType.received &&
newState.latestReceivedNotification.isBefore(tx.time)) {
// Future
final Contact from = tx.from;
NotificationController.notifyTransaction(
tx.time.millisecondsSinceEpoch.toString(),
amount: tx.amount,
currentUd: appCubit.currentUd,
from: from.title,
isG1: isG1);
emit(newState.copyWith(latestReceivedNotification: tx.time));
}
if (tx.type == TransactionType.sent &&
newState.latestSentNotification.isBefore(tx.time)) {
// Future
final Contact to = tx.to;
NotificationController.notifyTransaction(
tx.time.millisecondsSinceEpoch.toString(),
amount: -tx.amount,
currentUd: appCubit.currentUd,
to: to.title,
isG1: isG1);
emit(newState.copyWith(latestSentNotification: tx.time));
}
}
return newState.transactions;
}
if (!success) {
throw Exception('Failed to get transactions after $retries attempts');
}
// This should not be executed
return <Transaction>[];
}
String _getTxKey(Transaction t) => '${t.to.pubKey}-${t.comment}-${t.amount}';
@override
TransactionState fromJson(Map<String, dynamic> json) =>
TransactionState.fromJson(json);
@override
Map<String, dynamic> toJson(TransactionState state) => state.toJson();
List<Transaction> get transactions => state.transactions;
double get balance => state.balance;
DateTime get lastChecked => state.lastChecked;
void updatePendingTransaction(Transaction tx) {
final TransactionState currentState = state;
final List<Transaction> newPendingTransactions = <Transaction>[];
for (final Transaction t in state.pendingTransactions) {
if (tx.from == t.from &&
tx.to == t.to &&
tx.amount == t.amount &&
tx.comment == t.comment) {
newPendingTransactions
.add(t.copyWith(time: DateTime.now(), type: tx.type));
} else {
newPendingTransactions.add(t);
}
}
emit(currentState.copyWith(pendingTransactions: newPendingTransactions));
}
void insertPendingTransaction(Transaction tx) {
final TransactionState currentState = state;
final List<Transaction> newPendingTransactions = state.pendingTransactions;
newPendingTransactions.insert(0, tx);
emit(currentState.copyWith(pendingTransactions: newPendingTransactions));
}
}
import 'dart:async';
import 'package:get_it/get_it.dart';
import 'package:rxdart/rxdart.dart';
import '../../ui/logger.dart';
import '../../ui/widgets/connectivity_widget_wrapper_wrapper.dart';
import 'app_cubit.dart';
import 'multi_wallet_transaction_cubit.dart';
import 'node_list_cubit.dart';
import 'transaction.dart';
import 'utxo_cubit.dart';
part 'transactions_state.dart';
class TransactionsBloc {
TransactionsBloc({this.isExternal = false, this.pubKey}) {
TransactionsBloc(
{this.isExternal = false,
this.pubKey,
this.pageSize = 20,
required this.isV2}) {
_onPageRequest.stream
.flatMap(_fetchTransactionsList)
.listen(_onNewListingStateController.add)
......@@ -27,13 +29,8 @@ class TransactionsBloc {
final bool isExternal;
final String? pubKey;
late AppCubit appCubit;
late NodeListCubit nodeListCubit;
late MultiWalletTransactionCubit transCubit;
late UtxoCubit utxoCubit;
static const int _pageSize = 20;
final int pageSize;
final bool isV2;
final CompositeSubscription _subscriptions = CompositeSubscription();
......@@ -55,19 +52,9 @@ class TransactionsBloc {
Sink<String?> get onSearchInputChangedSink =>
_onSearchInputChangedSubject.sink;
// String? get _searchInputValue => _onSearchInputChangedSubject.value;
Stream<TransactionsState> _resetSearch() async* {
yield TransactionsState();
yield* _fetchTransactionsList(null);
}
void init(MultiWalletTransactionCubit transCubit, NodeListCubit nodeListCubit,
AppCubit appCubit, UtxoCubit utxoCubit) {
this.appCubit = appCubit;
this.transCubit = transCubit;
this.nodeListCubit = nodeListCubit;
this.utxoCubit = utxoCubit;
yield* _fetchTransactionsList(isV2 ? '0' : null);
}
Stream<TransactionsState> _fetchTransactionsList(String? pageKey) async* {
......@@ -77,6 +64,8 @@ class TransactionsBloc {
final bool isConnected =
await ConnectivityWidgetWrapperWrapper.isConnected;
logger('isConnected: $isConnected');
final MultiWalletTransactionCubit transCubit =
GetIt.instance<MultiWalletTransactionCubit>();
if (!isConnected) {
yield TransactionsState(
......@@ -85,18 +74,21 @@ class TransactionsBloc {
);
} else {
final List<Transaction> fetchedItems =
await transCubit.fetchTransactions(nodeListCubit, appCubit,
cursor: pageKey,
pageSize: _pageSize,
pubKey: pubKey,
isExternal: isExternal);
await transCubit.fetchTransactions(
cursor: isV2 ? _normalizePageKey(pageKey) : pageKey,
pageSize: pageSize,
pubKey: pubKey,
isExternal: isExternal,
);
final bool isLastPage = fetchedItems.length < _pageSize;
final String? nextPageKey =
isLastPage ? null : transCubit.currentWalletState(pubKey).endCursor;
final bool isLastPage = fetchedItems.length < pageSize;
final String? nextPageKey = isLastPage
? null
: (isV2
? ((int.tryParse(pageKey ?? '0') ?? 0) + pageSize).toString()
: transCubit.currentWalletState(pubKey).endCursor);
yield TransactionsState(
// error: null,
nextPageKey: nextPageKey,
itemList: pageKey == null
? fetchedItems
......@@ -115,6 +107,13 @@ class TransactionsBloc {
}
}
String? _normalizePageKey(String? pageKey) {
if (pageKey == null || int.tryParse(pageKey) == 0) {
return null;
}
return int.tryParse(pageKey)?.toString();
}
void dispose() {
_onSearchInputChangedSubject.close();
_onNewListingStateController.close();
......
......@@ -120,8 +120,8 @@ extension $UtxoCopyWith on Utxo {
Utxo _$UtxoFromJson(Map<String, dynamic> json) => Utxo(
txHash: json['txHash'] as String,
amount: (json['amount'] as num).toDouble(),
base: json['base'] as int,
outputIndex: json['outputIndex'] as int,
base: (json['base'] as num).toInt(),
outputIndex: (json['outputIndex'] as num).toInt(),
writtenTime: (json['writtenTime'] as num).toDouble(),
writtenBlock: (json['writtenBlock'] as num).toDouble(),
);
......
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import '../../ui/ui_helpers.dart';
import 'is_json_serializable.dart';
part 'credit_card_themes.g.dart';
part 'wallet_themes.g.dart';
class CreditCardThemes {
static const CreditCardTheme theme1 =
CreditCardTheme(Color(0xFF05112B), Color(0xFF085476));
static const CreditCardTheme theme2 = CreditCardTheme(
class WalletThemes {
static const WalletTheme theme1 =
WalletTheme(Color(0xFF05112B), Color(0xFF085476));
static const WalletTheme theme2 = WalletTheme(
Colors.blueGrey,
Colors.pink,
);
static const CreditCardTheme theme3 = CreditCardTheme(
static const WalletTheme theme3 = WalletTheme(
Color(0xFF00A9E0),
Color(0xFF0077B5),
);
static const CreditCardTheme theme4 = CreditCardTheme(
static const WalletTheme theme4 = WalletTheme(
Color(0xFFFDB813),
Color(0xFF8C1D40),
);
static const CreditCardTheme theme5 = CreditCardTheme(
static const WalletTheme theme5 = WalletTheme(
Colors.blueGrey,
Colors.deepPurple,
);
static const CreditCardTheme theme6 =
CreditCardTheme(Colors.blue, Colors.green);
static const CreditCardTheme theme7 = CreditCardTheme(
static const WalletTheme theme6 = WalletTheme(Colors.blue, Colors.green);
static const WalletTheme theme7 = WalletTheme(
Colors.black54,
Colors.black,
);
static const CreditCardTheme theme8 = CreditCardTheme(
static const WalletTheme theme8 = WalletTheme(
Colors.blueGrey,
Color(0xFF004678),
);
static const CreditCardTheme theme9 = CreditCardTheme(
static const WalletTheme theme9 = WalletTheme(
Color(0xFFCE002D),
Color(0xFF673F1E),
);
static const CreditCardTheme theme10 =
CreditCardTheme(Color(0xFF598040), Color(0xFF225500));
static const WalletTheme theme10 =
WalletTheme(Color(0xFF598040), Color(0xFF225500));
static const List<CreditCardTheme> themes = <CreditCardTheme>[
CreditCardThemes.theme1,
CreditCardThemes.theme2,
CreditCardThemes.theme3,
CreditCardThemes.theme4,
CreditCardThemes.theme5,
CreditCardThemes.theme6,
CreditCardThemes.theme7,
CreditCardThemes.theme8,
CreditCardThemes.theme9,
CreditCardThemes.theme10,
static const List<WalletTheme> themes = <WalletTheme>[
WalletThemes.theme1,
WalletThemes.theme2,
WalletThemes.theme3,
WalletThemes.theme4,
WalletThemes.theme5,
WalletThemes.theme6,
WalletThemes.theme7,
WalletThemes.theme8,
WalletThemes.theme9,
WalletThemes.theme10,
];
}
@JsonSerializable()
class CreditCardTheme implements IsJsonSerializable<CreditCardTheme> {
const CreditCardTheme(
class WalletTheme implements IsJsonSerializable<WalletTheme> {
const WalletTheme(
this.primaryColor,
this.secondaryColor,
);
factory CreditCardTheme.fromJson(Map<String, dynamic> json) =>
_$CreditCardThemeFromJson(json);
factory WalletTheme.fromJson(Map<String, dynamic> json) =>
_$WalletThemeFromJson(json);
@JsonKey(
name: 'primary_color', toJson: _colorToJson, fromJson: _colorFromJson)
......@@ -73,15 +73,15 @@ class CreditCardTheme implements IsJsonSerializable<CreditCardTheme> {
name: 'secondary_color', toJson: _colorToJson, fromJson: _colorFromJson)
final Color secondaryColor;
static int _colorToJson(Color color) => color.value;
static int _colorToJson(Color color) => colorToValue(color);
static Color _colorFromJson(int value) => Color(value);
@override
Map<String, dynamic> toJson() => _$CreditCardThemeToJson(this);
Map<String, dynamic> toJson() => _$WalletThemeToJson(this);
@override
CreditCardTheme fromJson(Map<String, dynamic> json) {
return CreditCardTheme.fromJson(json);
WalletTheme fromJson(Map<String, dynamic> json) {
return WalletTheme.fromJson(json);
}
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'credit_card_themes.dart';
part of 'wallet_themes.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CreditCardTheme _$CreditCardThemeFromJson(Map<String, dynamic> json) =>
CreditCardTheme(
CreditCardTheme._colorFromJson(json['primary_color'] as int),
CreditCardTheme._colorFromJson(json['secondary_color'] as int),
WalletTheme _$WalletThemeFromJson(Map<String, dynamic> json) => WalletTheme(
WalletTheme._colorFromJson((json['primary_color'] as num).toInt()),
WalletTheme._colorFromJson((json['secondary_color'] as num).toInt()),
);
Map<String, dynamic> _$CreditCardThemeToJson(CreditCardTheme instance) =>
Map<String, dynamic> _$WalletThemeToJson(WalletTheme instance) =>
<String, dynamic>{
'primary_color': CreditCardTheme._colorToJson(instance.primaryColor),
'secondary_color': CreditCardTheme._colorToJson(instance.secondaryColor),
'primary_color': WalletTheme._colorToJson(instance.primaryColor),
'secondary_color': WalletTheme._colorToJson(instance.secondaryColor),
};
......@@ -31,4 +31,8 @@ abstract class Env {
static const String endPoints = _Env.endPoints;
@EnviedField(varName: 'DUNITER_INDEXER_NODES')
static const String duniterIndexerNodes = _Env.duniterIndexerNodes;
@EnviedField(varName: 'DATAPOD_ENDPOINTS')
static const String datapodEndpoints = _Env.datapodEndpoints;
@EnviedField(varName: 'IPFS_GATEWAYS')
static const String ipfsGateways = _Env.ipfsGateways;
}
......@@ -12,131 +12,131 @@ final class _Env {
static const String currency = 'g1';
static const List<int> _enviedkeysentryDsn = <int>[
2758022540,
567599101,
1213250054,
1618631361,
1279400605,
1058553304,
3010455899,
1217112780,
3230594019,
210629287,
4176646343,
1335543012,
3764143899,
1219871740,
3768095478,
3770785191,
2459060282,
913808045,
3241323762,
3196695996,
3902320711,
1021343196,
1029148270,
622322169,
2976831774,
3473008884,
1387406286,
267373967,
2409251929,
329253784,
3633877812,
2440156896,
3702085230,
1372092264,
1989014989,
3474857978,
1383682462,
613503070,
3993440008,
4042027280,
3946539058,
3856740340,
47303277,
3002221340,
2057630474,
1745174984,
4240957329,
3437010089,
1781113053,
1324746536,
1127694165,
1424399078,
2175331971,
1404690130,
746348539,
1661294889,
545445384,
1843420850,
342001918,
2387570455,
3705407120,
3826182180,
3448174722,
2375811811,
1556105486,
3649627460,
2569872558,
3179512492,
1473258011,
1792161183,
1813247683,
1671469938,
2382346270,
583217726,
259915993,
2348588801,
31469926,
748485219,
1498358387,
1757676553,
300670654,
2044220030,
398092979,
2892502955,
2340727297,
3261443925,
4024944097,
3047157593,
1658471148,
450612405,
3642937030,
2571677048,
1679699856,
2894164369,
2775999580,
4286473772,
3222904016,
1235616023,
3040067773,
2648844337,
706717497,
881721290,
3597683196,
1671370095,
2634541862,
2030998593,
1306532544,
3746997137,
113708384,
212127156,
3494362847,
2187434371,
274263683,
3223494104,
3657696887,
2825789688,
726960873,
1828748106,
472395482,
2897777648,
4119607505,
173190659,
];
static const List<int> _envieddatasentryDsn = <int>[
2758022628,
567598985,
1213250162,
1618631345,
1279400686,
1058553314,
3010455924,
1217112803,
3230594000,
210629271,
4176646385,
1335542999,
3764143919,
1219871689,
3768095381,
3770785221,
2459060226,
913808026,
3241323671,
3196696025,
3902320755,
1021343161,
1029148255,
622322074,
2976831868,
3473008790,
1387406251,
267374006,
2409251945,
329253802,
3633877767,
2440156806,
3702085132,
1372092252,
1989014956,
3474857884,
1383682557,
613503083,
3993440110,
4042027379,
3946539122,
3856740231,
47303176,
3002221426,
2057630590,
1745174970,
4240957416,
3437010055,
1781113022,
1324746567,
1127694136,
1424398995,
2175332077,
1404690103,
746348424,
1661294855,
545445479,
1843420864,
342001817,
2387570488,
3705407142,
3826182220,
3448174838,
2375811735,
1556105598,
3649627447,
2569872532,
3179512451,
1473258036,
1792161196,
1813247731,
1671469892,
2382346285,
583217674,
259916012,
2348588898,
31469828,
748485211,
1498358340,
1757676652,
300670683,
2044219978,
398093014,
2892502938,
2340727394,
3261443895,
4024944003,
3047157564,
1658471125,
450612357,
3642937076,
2571677003,
1679699958,
2894164467,
2775999592,
4286473805,
3222903990,
1235616116,
3040067720,
2648844375,
706717530,
881721226,
3597683087,
1671369994,
2634541896,
2030998581,
1306532530,
3746997224,
113708366,
212127191,
3494362800,
2187434478,
274263798,
3223494070,
3657696786,
2825789579,
726960839,
1828748069,
472395432,
2897777559,
4119607550,
173190709,
];
static final String? sentryDsn = String.fromCharCodes(List<int>.generate(
......@@ -146,49 +146,49 @@ final class _Env {
).map((int i) => _envieddatasentryDsn[i] ^ _enviedkeysentryDsn[i]));
static const List<int> _enviedkeygitLabToken = <int>[
1276462360,
3147442616,
484401248,
2711833308,
2071256951,
413413379,
753293842,
2657052832,
801014742,
3072848070,
2632066142,
752047743,
1524780419,
3480689909,
3724499275,
4045798732,
868331394,
2572650952,
1541395842,
1522813345,
2006290859,
4281095164,
3777758143,
3497419970,
3518986845,
1103059359,
879406990,
1101534343,
769793526,
2188784112,
3991047156,
2090451402,
1064360920,
3663789202,
4216407525,
390169403,
3364586679,
231983091,
2644215881,
3791438002,
];
static const List<int> _envieddatagitLabToken = <int>[
1276462432,
3147442642,
484401176,
2711833220,
2071256867,
413413493,
753293857,
2657052922,
801014660,
3072848060,
2632066069,
752047628,
1524780512,
3480689857,
3724499224,
4045798684,
868331478,
2572650886,
1541395894,
1522813394,
2006290899,
4281095062,
3777758151,
3497419930,
3518986761,
1103059433,
879407037,
1101534429,
769793444,
2188784010,
3991047103,
2090451385,
1064360891,
3663789222,
4216407478,
390169451,
3364586723,
231983037,
2644215933,
3791438017,
];
static final String gitLabToken = String.fromCharCodes(List<int>.generate(
......@@ -198,17 +198,23 @@ final class _Env {
).map((int i) => _envieddatagitLabToken[i] ^ _enviedkeygitLabToken[i]));
static const String duniterNodes =
'https://g1.duniter.fr https://g1.le-sou.org https://g1.cgeek.fr https://g1.monnaielibreoccitanie.org https://g1.duniter.fr https://g1.le-sou.org https://g1.cgeek.fr';
'https://g1.duniter.org https://g1.le-sou.org https://g1.cgeek.fr https://g1.monnaielibreoccitanie.org https://g1.duniter.fr https://g1.le-sou.org https://g1.cgeek.fr';
static const String cesiumPlusNodes =
'https://g1.data.le-sou.org https://g1.data.e-is.pro https://g1.data.presles.fr https://g1.data.mithril.re https://g1.data.brussels.ovh https://g1.data.pini.fr';
static const String gvaNodes =
'https://g1v1.p2p.legal:443/gva https://g1.asycn.io:443/gva https://duniter.pini.fr:443/gva';
'https://g1v1.p2p.legal/gva https://g1.asycn.io/gva https://duniter.pini.fr/gva https://duniter-v1.comunes.net/gva';
static const String endPoints =
'wss://1000i100.fr/ws wss://gdev.librelois.fr/ws wss://gdev.p2p.legal/ws wss://gdev.coinduf.eu/ws wss://vit.fdn.org/ws wss://gdev.cgeek.fr/ws wss://gdev.pini.fr/ws';
'wss://gdev.cgeek.fr/ws wss://gdev.trentesaux.fr/ws wss://gdev.gyroi.de/ws wss://gdev-smith.pini.fr/ws wss://gdev.pini.fr/ws wss://gdev.txmn.tk/ws wss://duniter-v2-vjrj-gdev.comunes.net/ws wss://bulmagdev.sleoconnect.fr/ws wss://gdev.cuates.net/ws wss://gdev.matograine.fr/ws wss://gdev.p2p.legal/ws';
static const String duniterIndexerNodes =
'https://gdev-indexer.p2p.legal/v1/graphql https://hasura.gdev.coinduf.eu/v1/graphql http://gdev-hasura.cgeek.fr/v1/graphql';
'https://squid.gdev.coinduf.eu/v1/graphql https://squid.gdev.gyroi.de/v1/graphql https://duniter-v2-vjrj-squid.comunes.net/v1/graphql';
static const String datapodEndpoints =
'https://datapod.gyroi.de/v1/graphql https://datapod.coinduf.eu/v1/graphql';
static const String ipfsGateways =
'https://gyroi.de https://gateway.datapod.coinduf.eu/';
}
......@@ -3,12 +3,21 @@ import 'dart:io';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:duniter_datapod/duniter_datapod_client.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-queries.data.gql.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-queries.req.gql.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-queries.var.gql.dart';
import 'package:duniter_indexer/duniter_indexer_client.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.data.gql.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.req.gql.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.var.gql.dart';
import 'package:durt/durt.dart';
import 'package:ferry/ferry.dart' as ferry;
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'package:polkadart/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:tuple/tuple.dart';
import 'package:universal_html/html.dart' show window;
......@@ -18,18 +27,20 @@ import '../data/models/node.dart';
import '../data/models/node_lists_default.dart';
import '../data/models/node_manager.dart';
import '../data/models/node_type.dart';
import '../data/models/transaction_state.dart';
import '../data/models/utxo.dart';
import '../env.dart';
import '../graphql/__generated__/duniter-custom-queries.data.gql.dart';
import '../graphql/__generated__/duniter-custom-queries.req.gql.dart';
import '../graphql/__generated__/duniter-custom-queries.var.gql.dart';
import '../graphql/__generated__/duniter-indexer.schema.gql.dart';
import '../graphql/duniter_indexer_client.dart';
import '../shared_prefs_helper.dart';
import '../ui/contacts_cache.dart';
import '../ui/in_dev_helper.dart';
import '../ui/logger.dart';
import '../ui/ui_helpers.dart';
import 'duniter_endpoint_helper.dart';
import 'g1_helper.dart';
import 'no_nodes_exception.dart';
import 'node_check_result.dart';
import 'pay_result.dart';
import 'service_manager.dart';
// Tx history
// https://g1.duniter.org/tx/history/FadJvhddHL7qbRd3WcRPrWEJJwABQa3oZvmCBhotc7Kg
......@@ -50,33 +61,125 @@ Future<String> getTxHistory(String publicKey) async {
}
}
Future<Response> getPeers() async {
final Response response = (await requestWithRetry(
NodeType.duniter, '/network/peers',
dontRecord: true))
.item2;
if (response.statusCode == 200) {
return response;
} else {
throw Exception('Failed to load duniter node peers');
Future<List<dynamic>> getPeers(NodeType type) async {
// const Duration timeout = Duration(seconds: 10);
// Prevent concurrent modification
final List<Node> nodes = List<Node>.from(NodeManager().nodeList(type));
loggerDev('Fetching ${type.name} peers with peers ${nodes.length}');
List<dynamic> currentPeers = <dynamic>[];
for (final Node node in nodes) {
if (type == NodeType.duniter || type == NodeType.gva) {
String nodeUrl = node.url;
nodeUrl = nodeUrl.replaceAll(RegExp(r'/gva$'), '');
nodeUrl = '$nodeUrl/network/peers';
loggerDev('Fetching $nodeUrl');
try {
final Response response = await http.get(Uri.parse(nodeUrl));
if (response.statusCode == 200) {
// Try decode
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'] == currency)
.where((dynamic peer) =>
(peer as Map<String, dynamic>)['version'] == 10)
.where((dynamic peer) =>
(peer as Map<String, dynamic>)['status'] == 'UP')
.toList();
if (currentPeers.length < peers.length) {
// sometimes getPeers returns a small list of nodes (somethmes even one)
currentPeers = peers;
}
}
} catch (e) {
loggerDev('Error retrieving $nodeUrl ($e)');
// Ignore
}
} else if (type == NodeType.endpoint) {
try {
/* final PolkaDotProvider wsProvider =
PolkaDotProvider(Uri.parse('wss://rpc.polkadot.io'));
final MyWsSubstrateService wsService = MyWsSubstrateService(wsProvider);
final SubstrateRPC rpc = SubstrateRPC(wsService);
final SyncStateResponse syncState =
await rpc.request(const SubstrateRPCSystemSyncState());
syncState.currentBlock
print('Sync State: $syncState'); */
/*
final Provider polkadot = Provider.fromUri(Uri.parse(node.url));
final SystemApi<Provider, dynamic, dynamic> api =
SystemApi<Provider, dynamic, dynamic>(polkadot);
final List<PeerInfo<dynamic, dynamic>>? peers =
await api.peers().timeout(timeout);
for (final PeerInfo<dynamic, dynamic> peer in peers) {
print(peer);
}
final Health health = await api.health();
if (health.isSyncing) {
loggerDev('Node ${node.url} is syncing');
continue;
}
print('node ${node.url} has ${health.peers} peers');
*/
/* final Provider polkadot = Provider.fromUri(Uri.parse(node.url));
final SystemApi<Provider, dynamic, dynamic> api =
SystemApi<Provider, dynamic, dynamic>(polkadot);
final Health health = await api.health();
if (!health.isSyncing) {
final RpcResponse<dynamic, dynamic>? response =
await queryPolkadotNode(
nodeUri: node.url,
queryMethod: 'system_peers',
params: <dynamic>[],
timeout: timeout);
if (response != null && response.error != null) {
// final SyncState syncState = SyncState.fromJson(result.result as Map<String, dynamic>);
loggerDev('result ${response.result}');
}
}*/
} catch (e, stacktrace) {
loggerDev('Error retrieving peers from ${node.url} ($e)');
loggerDev(stacktrace);
// Ignore
}
}
}
return currentPeers;
}
Future<Response> searchCPlusUser(String initialSearchTerm) async {
final String searchTerm = normalizeQuery(initialSearchTerm);
final String searchTermLower = searchTerm.toLowerCase();
final String searchTermCapitalized =
searchTermLower[0].toUpperCase() + searchTermLower.substring(1);
Future<List<Contact>> searchProfilesV1(
{required String searchTermLower,
required String searchTerm,
required String searchTermCapitalized}) async {
final String query =
'/user/profile/_search?q=title:$searchTermLower OR issuer:$searchTerm OR title:$searchTermCapitalized OR title:$searchTerm';
final Response response =
(await requestCPlusWithRetry(query, retryWith404: false)).item2;
return response;
final List<Contact> searchResult = <Contact>[];
if (response.statusCode != 404) {
// Add cplus users
final List<dynamic> hits = ((const JsonDecoder().convert(response.body)
as Map<String, dynamic>)['hits'] as Map<String, dynamic>)['hits']
as List<dynamic>;
for (final dynamic hit in hits) {
final Contact c = await contactFromResultSearch(
hit as Map<String, dynamic>,
);
logger('Contact retrieved in c+ search $c');
ContactsCache().addContact(c);
searchResult.add(c);
}
}
return searchResult;
}
Future<Contact> getProfile(String pubKeyRaw,
Future<Contact> getProfileV1(String pubKeyRaw,
{bool onlyCPlusProfile = false, bool resize = true}) async {
final String pubKey = extractPublicKey(pubKeyRaw);
try {
......@@ -152,95 +255,56 @@ Future<List<Contact>> searchWotV1(String initialSearchTerm) async {
return contacts;
}
Future<List<Contact>> searchWotV2(String namePattern) async {
final List<Contact> contacts = <Contact>[];
final GAccountsByNameOrPkReq req = GAccountsByNameOrPkReq(
(GAccountsByNameOrPkReqBuilder b) => b..vars.pattern = namePattern);
final ferry.Client client = await initDuniterIndexerClient(
_getBestNodes(NodeType.duniterIndexer).first.url);
final ferry
.OperationResponse<GAccountsByNameOrPkData, GAccountsByNameOrPkVars>
response = await client.request(req).first;
if (response.hasErrors) {
loggerDev('Error: ${response.linkException?.originalException}');
} else {
final GAccountsByNameOrPkData? accounts = response.data;
for (final GAccountsByNameOrPkData_account account in accounts!.account) {
final String? pubkey = account.identity?.account?.identity?.pubkey;
if (pubkey == null) {
loggerDev('ERROR: Pubkey is null');
} else {
contacts.add(Contact(
nick: account.identity?.account?.identity?.name, pubKey: pubkey));
}
}
}
return contacts;
}
@Deprecated('use getProfile')
Future<String> _getDataImageFromKey(String publicKey) async {
final Response response =
(await requestCPlusWithRetry('/user/profile/$publicKey')).item2;
if (response.statusCode == HttpStatus.ok) {
final Map<String, dynamic> data =
json.decode(response.body) as Map<String, dynamic>;
final Map<String, dynamic> source = data['_source'] as Map<String, dynamic>;
if (source.containsKey('avatar')) {
final Map<String, dynamic> avatarData =
source['avatar'] as Map<String, dynamic>;
if (avatarData.containsKey('_content')) {
final String content = avatarData['_content'] as String;
return 'data:image/png;base64,$content';
}
}
}
throw Exception('Failed to load avatar');
}
Uint8List imageFromBase64String(String base64String) {
return Uint8List.fromList(
base64Decode(base64String.substring(base64String.indexOf(',') + 1)));
}
@Deprecated('use getProfile')
Future<Uint8List> getAvatar(String pubKey) async {
final String dataImage = await _getDataImageFromKey(pubKey);
return imageFromBase64String(dataImage);
}
Future<void> fetchNodesIfNotReady() async {
final List<Future<void>> fetchFutures = <Future<void>>[];
for (final NodeType type in <NodeType>[
NodeType.gva,
NodeType.duniter,
NodeType.endpoint,
NodeType.duniterIndexer
NodeType.duniterIndexer,
NodeType.cesiumPlus,
NodeType.datapodEndpoint,
NodeType.ipfsGateway
]) {
if (NodeManager().nodesWorking(type) < 3) {
await fetchNodes(type, true);
fetchFutures.add(fetchNodes(type, true));
}
}
await Future.wait(fetchFutures);
}
Future<void> fetchNodes(NodeType type, bool force) async {
try {
if (type == NodeType.duniter) {
_fetchDuniterNodes(force: force);
} else {
if (type == NodeType.cesiumPlus) {
_fetchCesiumPlusNodes(force: force);
}
if (type == NodeType.endpoint) {
_fetchEndPointNodes(force: force);
}
if (type == NodeType.duniterIndexer) {
_fetchDuniterIndexerNodes(force: force);
} else {
_fetchGvaNodes(force: force);
}
final List<Future<void>> fetchFutures = <Future<void>>[];
switch (type) {
case NodeType.duniter:
fetchFutures.add(_fetchDuniterNodes(force: force));
break;
case NodeType.gva:
fetchFutures.add(_fetchGvaNodes(force: force));
break;
case NodeType.cesiumPlus:
fetchFutures.add(_fetchCesiumPlusNodes(force: force));
break;
case NodeType.endpoint:
fetchFutures.add(_fetchEndPointNodes(force: force));
break;
case NodeType.duniterIndexer:
fetchFutures
.add(_fetchV2Nodes(type: NodeType.duniterIndexer, force: force));
break;
case NodeType.datapodEndpoint:
fetchFutures
.add(_fetchV2Nodes(type: NodeType.datapodEndpoint, force: force));
break;
case NodeType.ipfsGateway:
fetchFutures.add(_fetchIpfsGateways(force: force));
break;
}
await Future.wait(fetchFutures);
} on NoNodesException catch (e, stacktrace) {
logger(e.cause);
await Sentry.captureException(e, stackTrace: stacktrace);
......@@ -269,6 +333,20 @@ Future<void> _fetchDuniterNodes({bool force = false}) async {
NodeManager().loading = false;
}
Future<void> _fetchGvaNodes({bool force = false}) async {
NodeManager().loading = true;
const NodeType type = NodeType.gva;
if (force) {
NodeManager().updateNodes(type, defaultGvaNodes);
logger('Fetching gva nodes forced');
} else {
logger('Fetching gva nodes, we have ${NodeManager().nodesWorking(type)}');
}
final List<Node> nodes = await _fetchDuniterNodesFromPeers(type);
NodeManager().updateNodes(type, nodes);
NodeManager().loading = false;
}
// https://github.com/duniter/cesium/blob/467ec68114be650cd1b306754c3142fc4020164c/www/js/config.js#L96
// https://g1.data.le-sou.org/g1/peer/_search?pretty
Future<void> _fetchCesiumPlusNodes({bool force = false}) async {
......@@ -286,44 +364,42 @@ Future<void> _fetchCesiumPlusNodes({bool force = false}) async {
NodeManager().loading = false;
}
Future<void> _fetchGvaNodes({bool force = false}) async {
Future<void> _fetchEndPointNodes({bool force = false}) async {
NodeManager().loading = true;
const NodeType type = NodeType.gva;
const NodeType type = NodeType.endpoint;
if (force) {
NodeManager().updateNodes(type, defaultGvaNodes);
logger('Fetching gva nodes forced');
NodeManager().updateNodes(type, defaultEndPointNodes);
logger('Fetching endPoint nodes forced');
} else {
logger('Fetching gva nodes, we have ${NodeManager().nodesWorking(type)}');
logger(
'Fetching endPoint nodes, we have ${NodeManager().nodesWorking(type)}');
}
final List<Node> nodes = await _fetchDuniterNodesFromPeers(type);
final List<Node> nodes = await _fetchNodes(type);
// FIXME (this does not return urls)
// await getPeers(type);
NodeManager().updateNodes(type, nodes);
NodeManager().loading = false;
}
Future<void> _fetchEndPointNodes({bool force = false}) async {
Future<void> _fetchV2Nodes({required NodeType type, bool force = false}) async {
NodeManager().loading = true;
const NodeType type = NodeType.endpoint;
if (force) {
NodeManager().updateNodes(type, defaultEndPointNodes);
logger('Fetching endPoint nodes forced');
NodeManager().updateNodes(type, defaultNodes(type));
logger('Fetching $type nodes forced');
} else {
logger(
'Fetching endPoint nodes, we have ${NodeManager().nodesWorking(type)}');
logger('Fetching $type nodes, we have ${NodeManager().nodesWorking(type)}');
}
final List<Node> nodes = await _fetchNodes(type);
NodeManager().updateNodes(type, nodes);
NodeManager().loading = false;
}
Future<void> _fetchDuniterIndexerNodes({bool force = false}) async {
Future<void> _fetchIpfsGateways({required bool force}) async {
NodeManager().loading = true;
const NodeType type = NodeType.duniterIndexer;
const NodeType type = NodeType.ipfsGateway;
if (force) {
NodeManager().updateNodes(type, defaultDuniterIndexerNodes);
logger('Fetching duniter indexer nodes forced');
} else {
logger(
'Fetching duniter indexer nodes, we have ${NodeManager().nodesWorking(type)}');
NodeManager().updateNodes(type, defaultNodes(type));
logger('Fetching $type nodes forced');
}
final List<Node> nodes = await _fetchNodes(type);
NodeManager().updateNodes(type, nodes);
......@@ -333,72 +409,62 @@ Future<void> _fetchDuniterIndexerNodes({bool force = false}) async {
Future<List<Node>> _fetchDuniterNodesFromPeers(NodeType type,
{bool debug = false}) async {
logger('Fetching ${type.name} nodes from peers');
// const Duration timeout = Duration(seconds: 10);
final List<Node> lNodes = <Node>[];
final String apyType = (type == NodeType.duniter) ? 'BMAS' : 'GVA S';
// To compare with something...
String? fastestNode;
late Duration fastestLatency = const Duration(minutes: 1);
try {
final Response response = await getPeers();
if (response.statusCode == 200) {
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'] == currency)
.where(
(dynamic peer) => (peer as Map<String, dynamic>)['version'] == 10)
.where((dynamic peer) =>
(peer as Map<String, dynamic>)['status'] == 'UP')
.toList();
// reorder peer list
peers.shuffle();
for (final dynamic peerR in peers) {
final Map<String, dynamic> peer = peerR as Map<String, dynamic>;
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(apyType)) {
final String endpointUnParsed = endpoints[j];
final String? endpoint = parseHost(endpointUnParsed);
if (endpoint != null &&
// !endpoint.contains('test') &&
!endpoint.contains('localhost')) {
try {
final NodeCheck nodeCheck = await _pingNode(endpoint, type);
final Duration latency = nodeCheck.latency;
loggerD(debug,
'Evaluating node: $endpoint, latency ${latency.inMicroseconds} currentBlock: ${nodeCheck.currentBlock}');
final Node node = Node(
url: endpoint,
latency: latency.inMicroseconds,
currentBlock: nodeCheck.currentBlock);
if (fastestNode == null || latency < fastestLatency) {
fastestNode = endpoint;
fastestLatency = latency;
if (!kReleaseMode) {
loggerD(
debug, 'Node bloc: Current faster node $fastestNode');
}
NodeManager().insertNode(type, node);
lNodes.insert(0, node);
} else {
// Not the faster
NodeManager().addNode(type, node);
lNodes.add(node);
final List<dynamic> peers = await getPeers(type);
// reorder peer list
peers.shuffle();
for (final dynamic peerR in peers) {
final Map<String, dynamic> peer = peerR as Map<String, dynamic>;
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(apyType)) {
final String endpointUnParsed = endpoints[j];
final String? endpoint = parseHost(endpointUnParsed);
if (endpoint != null &&
// !endpoint.contains('test') &&
!endpoint.contains('localhost')) {
try {
final NodeCheckResult nodeCheck =
await _pingNode(endpoint, type);
final Duration latency = nodeCheck.latency;
loggerD(debug,
'Evaluating node: $endpoint, latency ${latency.inMicroseconds} currentBlock: ${nodeCheck.currentBlock}');
final Node node = Node(
url: endpoint,
latency: latency.inMicroseconds,
currentBlock: nodeCheck.currentBlock);
if (fastestNode == null || latency < fastestLatency) {
fastestNode = endpoint;
fastestLatency = latency;
if (!kReleaseMode) {
loggerD(
debug, 'Node bloc: Current faster node $fastestNode');
}
} catch (e) {
logger('Error fetching $endpoint, error: $e');
NodeManager().insertNode(type, node);
lNodes.insert(0, node);
} else {
// Not the faster
NodeManager().addNode(type, node);
lNodes.add(node);
}
} catch (e) {
logger('Error fetching $endpoint, error: $e');
}
}
}
if (kReleaseMode && lNodes.length >= NodeManager.maxNodes) {
// In production dont' get too much nodes
loggerD(debug, 'We have enough ${type.name} nodes for now');
break;
}
}
if (kReleaseMode && lNodes.length >= NodeManager.maxNodes) {
// In production dont' get too much nodes
loggerD(debug, 'We have enough ${type.name} nodes for now');
break;
}
}
}
......@@ -435,7 +501,8 @@ Future<List<Node>> _fetchNodes(NodeType type) async {
for (final Node node in currentNodes) {
final String endpoint = node.url;
try {
final NodeCheck nodeCheck = await _pingNode(endpoint, type);
logger('Evaluating node: $endpoint');
final NodeCheckResult nodeCheck = await _pingNode(endpoint, type);
final Duration latency = nodeCheck.latency;
logger('Evaluating node: $endpoint, latency ${latency.inMicroseconds}');
final Node node = Node(
......@@ -459,9 +526,10 @@ Future<List<Node>> _fetchNodes(NodeType type) async {
logger('Error fetching $endpoint, error: $e');
}
}
logger(
'Fetched ${lNodes.length} ${type.name} nodes ordered by latency (first: ${lNodes.first.url})');
if (lNodes.isNotEmpty) {
logger(
'Fetched ${lNodes.length} ${type.name} nodes ordered by latency (first: ${lNodes.first.url})');
}
} catch (e, stacktrace) {
await Sentry.captureException(e, stackTrace: stacktrace);
logger('General error in fetch ${type.name}: $e');
......@@ -472,114 +540,43 @@ Future<List<Node>> _fetchNodes(NodeType type) async {
return lNodes;
}
Future<NodeCheck> _pingNode(String node, NodeType type) async {
// Decrease timeout during ping
Future<NodeCheckResult> _pingNode(String node, NodeType type) async {
const Duration timeout = Duration(seconds: 10);
int currentBlock = 0;
Duration latency;
final Map<NodeType,
Future<NodeCheckResult> Function(String node, Duration timeout)>
testFunctions = <NodeType,
Future<NodeCheckResult> Function(String node, Duration timeout)>{
NodeType.duniter: testDuniterV1Node,
NodeType.cesiumPlus: testCPlusV1Node,
NodeType.gva: testGVAV1Node,
NodeType.endpoint: testEndPointV2,
NodeType.duniterIndexer: testDuniterIndexerV2,
NodeType.datapodEndpoint: testDuniterDatapodV2,
NodeType.ipfsGateway: testIpfsGateway
};
final Future<NodeCheckResult> Function(String node, Duration timeout)
testFunction = testFunctions[type] ?? testDuniterIndexerV2;
try {
final Stopwatch stopwatch = Stopwatch()..start();
if (type == NodeType.duniter) {
final Response response = await http
.get(Uri.parse('$node/blockchain/current'))
.timeout(timeout);
stopwatch.stop();
latency = stopwatch.elapsed;
if (response.statusCode == 200) {
final Map<String, dynamic> json =
jsonDecode(response.body) as Map<String, dynamic>;
currentBlock = json['number'] as int;
} else {
latency = wrongNodeDuration;
}
} else if (type == NodeType.cesiumPlus) {
// see: http://g1.data.e-is.pro/network/peering
final Response response = await http
.get(Uri.parse('$node/node/stats'))
// Decrease http timeout during ping
.timeout(timeout);
if (response.statusCode == 200) {
try {
final Map<String, dynamic> json =
jsonDecode(response.body.replaceAll('"cluster"{', '"cluster": {'))
as Map<String, dynamic>;
currentBlock = ((((json['stats'] as Map<String, dynamic>)['cluster']
as Map<String, dynamic>)['indices']
as Map<String, dynamic>)['docs']
as Map<String, dynamic>)['count'] as int;
} catch (e) {
loggerDev('Cannot parse node/stats $e');
}
} else {
latency = wrongNodeDuration;
}
stopwatch.stop();
latency = stopwatch.elapsed;
} else if (type == NodeType.endpoint) {
if (!kIsWeb) {
try {
final Provider polkadot = Provider(Uri.parse(node));
// From:
// https://github.com/leonardocustodio/polkadart/blob/main/examples/bin/extrinsic_demo.dart
final RpcResponse<dynamic> block =
await polkadot.send('chain_getBlock', <dynamic>[]);
currentBlock = int.parse(
(((block.result as Map<String, dynamic>)['block']
as Map<String, dynamic>)['header']
as Map<String, dynamic>)['number'] as String);
stopwatch.stop();
latency = stopwatch.elapsed;
await polkadot.disconnect();
} catch (e) {
loggerDev('Cannot parse node/stats $e');
latency = wrongNodeDuration;
}
} else {
// Waiting for web support in polkadart:
// https://github.com/leonardocustodio/polkadart/issues/297
latency = wrongNodeDuration;
}
} else if (type == NodeType.duniterIndexer) {
final ferry.Client client = await initDuniterIndexerClient(node);
final ferry.OperationResponse<GLastIndexedBlockNumberData,
GLastIndexedBlockNumberVars> response =
await client.request(GLastIndexedBlockNumberReq()).first;
if (response.hasErrors) {
latency = wrongNodeDuration;
loggerDev('HAS ERRORS');
loggerDev(response.linkException!.originalException);
} else {
final Gjsonb? lastIndexedBlockNumber =
response.data?.parameters_by_pk!.value;
loggerDev(lastIndexedBlockNumber?.value);
if (lastIndexedBlockNumber?.value is num) {
currentBlock = (lastIndexedBlockNumber!.value as num).toInt();
latency = stopwatch.elapsed;
} else {
latency = wrongNodeDuration;
}
}
} else {
// Test GVA with a query
final Gva gva = Gva(node: proxyfyNode(node));
currentBlock = await gva.getCurrentBlock().timeout(timeout);
// NodeManager().updateNode(type, node.copyWith(latency: newLatency));
stopwatch.stop();
final double balance = await gva
.balance('78ZwwgpgdH5uLZLbThUQH7LKwPgjMunYfLiCfUCySkM8')
.timeout(timeout);
latency = balance >= 0 ? stopwatch.elapsed : wrongNodeDuration;
}
logger(
'Ping tested in node $node ($type), latency ${latency.inMicroseconds}, current block $currentBlock');
return NodeCheck(latency: latency, currentBlock: currentBlock);
final NodeCheckResult result = await testFunction(node, timeout);
_logNodePing(node, type, result.latency, result.currentBlock);
return NodeCheckResult(
latency: result.latency, currentBlock: result.currentBlock);
} catch (e) {
// Handle exception when node is unavailable etc
logger('Node $node does not respond to ping $e');
return NodeCheck(latency: wrongNodeDuration, currentBlock: 0);
logger(
'Node $node does not respond to ping: ${removeNewlines(e.toString())}');
return NodeCheckResult(latency: wrongNodeDuration, currentBlock: 0);
}
}
void _logNodePing(
String node, NodeType type, Duration latency, int currentBlock) {
logger(
'Ping tested in node $node ($type), latency ${latency.inMicroseconds}, current block $currentBlock');
}
Future<Tuple2<Node, http.Response>> requestWithRetry(NodeType type, String path,
{bool dontRecord = false, bool retryWith404 = true}) async {
return _requestWithRetry(type, path, dontRecord, retryWith404);
......@@ -682,7 +679,7 @@ Future<Tuple2<Node, http.Response>> _requestWithRetry(
'Cannot make the request to any of the ${nodes.length} nodes');
}
Future<PayResult> payWithGVA(
Future<PayResult> payV1(
{required List<String> to, required double amount, String? comment}) async {
try {
final Tuple2<String, Node> selected = getGvaNode();
......@@ -750,25 +747,24 @@ Future<PayResult> payWithGVA(
}
Tuple2<String, Node> getGvaNode() {
final List<Node> nodes = _getBestNodes(NodeType.gva);
final List<Node> nodes = NodeManager().getBestNodes(NodeType.gva);
if (nodes.isNotEmpty) {
final Node? currentGvaNode = NodeManager().getCurrentGvaNode();
final Node node = currentGvaNode ?? nodes.first;
NodeManager().setCurrentGvaNode(node);
return Tuple2<String, Node>(proxyfyNode(node.url), node);
final bool currentIsInBest = nodes.contains(currentGvaNode);
final Node newNode =
currentIsInBest ? currentGvaNode ?? nodes.first : nodes.first;
loggerDev(
'New GVA node ${newNode.url} and currentGva ${currentGvaNode ?? const Node(url: "No node").url} is in best nodes: $currentIsInBest');
NodeManager().setCurrentGvaNode(newNode);
loggerDev('New GVA node ${newNode.url}');
return Tuple2<String, Node>(proxyfyNode(newNode.url), newNode);
} else {
throw Exception(
'Sorry: I cannot find a working node to send the transaction');
}
}
class PayResult {
PayResult({required this.message, this.node});
final Node? node;
final String message;
}
String proxyfyNode(String nodeUrl) {
final String url = inProduction && kIsWeb
? '${window.location.protocol}//${window.location.hostname}/proxy/${nodeUrl.replaceFirst('https://', '').replaceFirst('http://', '')}/'
......@@ -776,14 +772,18 @@ String proxyfyNode(String nodeUrl) {
return url;
}
Future<Tuple2<Map<String, dynamic>?, Node>> gvaHistoryAndBalance(
Future<Tuple2<Map<String, dynamic>?, Node>> getHistoryAndBalanceV1(
String pubKeyRaw,
[int? pageSize,
String? cursor]) async {
logger('Get tx history (page size: $pageSize: cursor $cursor)');
{int? pageSize,
int? from,
int? to,
String? cursor,
required bool isConnected}) async {
logger(
'Get tx history (page size: $pageSize: cursor $cursor, from: $from, to: $to)');
final String pubKey = extractPublicKey(pubKeyRaw);
return gvaFunctionWrapper<Map<String, dynamic>>(
(Gva gva) => gva.history(pubKey, pageSize, cursor));
return gvaFunctionWrapper<Map<String, dynamic>>((Gva gva) => gva
.history(pubKey, pageSize: pageSize, cursor: cursor, from: from, to: to));
}
Future<Tuple2<double?, Node>> gvaBalance(String pubKey) async {
......@@ -802,13 +802,18 @@ Future<Tuple2<Map<String, dynamic>?, Node>> getCurrentBlockGVA() async {
Future<Tuple2<T?, Node>> gvaFunctionWrapper<T>(
Future<T?> Function(Gva) specificFunction) async {
final List<Node> nodes = _getBestNodes(NodeType.gva);
final List<Node> nodes = NodeManager().getBestNodes(NodeType.gva);
// Try first the current GVA node
final Node? currentGvaNode = NodeManager().getCurrentGvaNode();
if (currentGvaNode != null) {
nodes.remove(currentGvaNode);
nodes.insert(0, currentGvaNode);
// Try to put the current Node first
final bool currentBlockSynced = nodes.isNotEmpty &&
currentGvaNode.currentBlock >= nodes.first.currentBlock;
if (currentBlockSynced) {
nodes.remove(currentGvaNode);
nodes.insert(0, currentGvaNode);
}
}
for (int i = 0; i < nodes.length; i++) {
......@@ -833,36 +838,6 @@ Future<Tuple2<T?, Node>> gvaFunctionWrapper<T>(
throw Exception('Sorry: I cannot find a working gva node');
}
List<Node> _getBestNodes(NodeType type) {
final List<Node> fnodes = NodeManager().nodesWorkingList(type);
final int maxCurrentBlock = fnodes.fold(
0,
(int max, Node node) =>
node.currentBlock > max ? node.currentBlock : max);
final List<Node> nodesAtMaxBlock = fnodes
.where((Node node) => node.currentBlock == maxCurrentBlock)
.toList();
nodesAtMaxBlock.sort((Node a, Node b) {
final int errorComparison = a.errors.compareTo(b.errors);
if (errorComparison != 0) {
return errorComparison;
} else {
return a.latency.compareTo(b.latency);
}
});
if (nodesAtMaxBlock.isEmpty) {
nodesAtMaxBlock.addAll(defaultNodes(type));
}
return nodesAtMaxBlock;
}
class NodeCheck {
NodeCheck({required this.latency, required this.currentBlock});
final Duration latency;
final int currentBlock;
}
void increaseNodeErrors(NodeType type, Node node) {
NodeManager().increaseNodeErrors(type, node);
}
......@@ -873,12 +848,12 @@ void increaseNodeErrors(NodeType type, Node node) {
// Add a new profile: user/profile (POST)
// Update an existing profile: user/profile/_update (POST)
// Delete an existing profile: user/profile/_delete (DELETE?)
Future<void> createOrUpdateCesiumPlusUser(String name) async {
Future<bool> createOrUpdateProfileV1(String name) async {
final CesiumWallet wallet = await SharedPreferencesHelper().getWallet();
final String pubKey = wallet.pubkey;
// Check if the user exists
final String? userName = await getCesiumPlusUser(pubKey);
final String? userName = await getProfileUserName(pubKey);
// Prepare the user profile data
final Map<String, dynamic> userProfile = <String, dynamic>{
......@@ -909,10 +884,12 @@ Future<void> createOrUpdateCesiumPlusUser(String name) async {
.item2;
if (updateResponse.statusCode == 200) {
logger('User profile updated successfully.');
return true;
} else {
logger(
'Failed to update user profile. Status code: ${updateResponse.statusCode}');
logger('Response body: ${updateResponse.body}');
return false;
}
} else if (userName == null) {
logger('User does not exist, create a new user profile');
......@@ -925,11 +902,14 @@ Future<void> createOrUpdateCesiumPlusUser(String name) async {
if (createResponse.statusCode == 200) {
logger('User profile created successfully.');
return true;
} else {
logger(
'Failed to create user profile. Status code: ${createResponse.statusCode}');
return false;
}
}
return false;
}
Map<String, String> _defCPlusHeaders() {
......@@ -947,12 +927,13 @@ void hashAndSign(Map<String, dynamic> data, CesiumWallet wallet) {
data['signature'] = signature;
}
Future<String?> getCesiumPlusUser(String pubKey) async {
final Contact c = await getProfile(pubKey, onlyCPlusProfile: true);
Future<String?> getProfileUserNameV1(String pubKey) async {
final Contact c =
await getProfile(pubKey, onlyProfile: true, complete: false);
return c.name;
}
Future<bool> deleteCesiumPlusUser() async {
Future<bool> deleteProfileV1() async {
final CesiumWallet wallet = await SharedPreferencesHelper().getWallet();
final String pubKey = wallet.pubkey;
final Map<String, dynamic> userProfile = <String, dynamic>{
......@@ -1114,3 +1095,196 @@ Future<PayResult> payWithBMA({
return PayResult(message: "Something didn't work as expected ($e)");
}
}
Future<NodeCheckResult> testDuniterIndexerV2(
String node, Duration timeout) async {
NodeCheckResult result;
final Stopwatch stopwatch = Stopwatch()..start();
final ferry.Client client = await initDuniterIndexerClient(node);
final ferry.OperationResponse<GLastBlockData, GLastBlockVars> response =
await client.request(GLastBlockReq()).first.timeout(timeout);
if (response.hasErrors) {
loggerDev(
'Node $node has errors: ${removeNewlines(response.linkException!.originalException.toString())}');
result = NodeCheckResult(currentBlock: 0, latency: wrongNodeDuration);
} else {
final int currentBlock = response.data?.block.first.height ?? 0;
result = NodeCheckResult(
currentBlock: currentBlock,
latency: currentBlock > 0 ? stopwatch.elapsed : wrongNodeDuration);
}
return result;
}
Future<NodeCheckResult> testDuniterDatapodV2(
String node, Duration timeout) async {
NodeCheckResult result;
final Stopwatch stopwatch = Stopwatch()..start();
final ferry.Client client =
await initDuniterDatapodClient(node, GetIt.instance<HiveStore>());
final ferry.OperationResponse<GGetProfileCountData, GGetProfileCountVars>
response =
await client.request(GGetProfileCountReq()).first.timeout(timeout);
if (response.hasErrors) {
loggerDev(
'Node $node has errors: ${removeNewlines(response.linkException!.originalException.toString())}');
result = NodeCheckResult(currentBlock: 0, latency: wrongNodeDuration);
} else {
final int currentBlock =
response.data?.profiles_aggregate.aggregate?.count ?? 0;
result = NodeCheckResult(
currentBlock: currentBlock,
latency: currentBlock > 0 ? stopwatch.elapsed : wrongNodeDuration);
}
return result;
}
Future<NodeCheckResult> testIpfsGateway(String node, Duration timeout) async {
final Stopwatch stopwatch = Stopwatch()..start();
final Response response = await http.get(Uri.parse(node)).timeout(timeout);
stopwatch.stop();
final Duration latency = stopwatch.elapsed;
final int currentBlock = response.statusCode;
final NodeCheckResult result =
NodeCheckResult(latency: latency, currentBlock: currentBlock);
return result;
}
Future<NodeCheckResult> testGVAV1Node(String node, Duration timeout) async {
final Stopwatch stopwatch = Stopwatch()..start();
// Test GVA with a query
final Gva gva = Gva(node: proxyfyNode(node));
final int currentBlock = await gva.getCurrentBlock().timeout(timeout);
// NodeManager().updateNode(type, node.copyWith(latency: newLatency));
stopwatch.stop();
final double balance = await gva
.balance('78ZwwgpgdH5uLZLbThUQH7LKwPgjMunYfLiCfUCySkM8')
.timeout(timeout);
final Duration latency = balance >= 0 ? stopwatch.elapsed : wrongNodeDuration;
final NodeCheckResult result =
NodeCheckResult(latency: latency, currentBlock: currentBlock);
return result;
}
Future<NodeCheckResult> testCPlusV1Node(String node, Duration timeout) async {
int currentBlock = 0;
Duration latency;
final Stopwatch stopwatch = Stopwatch()..start();
// see: http://g1.data.e-is.pro/network/peering
final Response response = await http
.get(Uri.parse('$node/node/stats'))
// Decrease http timeout during ping
.timeout(timeout);
if (response.statusCode == 200) {
try {
final Map<String, dynamic> json =
jsonDecode(response.body.replaceAll('"cluster"{', '"cluster": {'))
as Map<String, dynamic>;
currentBlock = ((((json['stats'] as Map<String, dynamic>)['cluster']
as Map<String, dynamic>)['indices']
as Map<String, dynamic>)['docs'] as Map<String, dynamic>)['count']
as int;
} catch (e) {
loggerDev('Cannot parse node/stats ${removeNewlines(e.toString())}');
}
} else {
latency = wrongNodeDuration;
}
stopwatch.stop();
latency = stopwatch.elapsed;
return NodeCheckResult(latency: latency, currentBlock: currentBlock);
}
Future<NodeCheckResult> testDuniterV1Node(String node, Duration timeout) async {
int currentBlock = 0;
Duration latency;
final Stopwatch stopwatch = Stopwatch()..start();
final Response response =
await http.get(Uri.parse('$node/blockchain/current')).timeout(timeout);
stopwatch.stop();
latency = stopwatch.elapsed;
if (response.statusCode == 200) {
final Map<String, dynamic> json =
jsonDecode(response.body) as Map<String, dynamic>;
currentBlock = json['number'] as int;
} else {
latency = wrongNodeDuration;
}
return NodeCheckResult(latency: latency, currentBlock: currentBlock);
}
Future<Contact> getProfile(String pubKeyRaw,
{bool onlyProfile = false,
bool resize = true,
required bool complete}) async {
return GetIt.instance<ServiceManager>().current.getProfile(
pubKeyRaw,
onlyProfile: onlyProfile,
resize: resize,
complete: complete,
);
}
Future<List<Contact>> searchWot(String searchPattern) async {
return GetIt.instance<ServiceManager>().current.searchWot(searchPattern);
}
Future<List<Contact>> searchProfiles(String initialSearchTerm) async {
final String searchTerm = normalizeQuery(initialSearchTerm);
final String searchTermLower = searchTerm.toLowerCase();
final String searchTermCapitalized =
searchTermLower[0].toUpperCase() + searchTermLower.substring(1);
return GetIt.instance<ServiceManager>().current.searchProfiles(
searchTermLower: searchTermLower,
searchTerm: searchTerm,
searchTermCapitalized: searchTermCapitalized);
}
Future<List<Contact>> getProfiles(List<String> pubKeys) async {
return GetIt.instance<ServiceManager>().current.getProfiles(pubKeys);
}
Future<Tuple2<Map<String, dynamic>?, Node>> getHistoryAndBalance(
String pubKeyRaw,
{int? pageSize,
int? from,
int? to,
String? cursor,
required bool isConnected}) {
return GetIt.instance<ServiceManager>().current.getHistoryAndBalance(
pubKeyRaw,
pageSize: pageSize,
from: from,
to: to,
cursor: cursor,
isConnected: isConnected);
}
Future<TransactionState> transactionsParser(
Map<String, dynamic> txData, TransactionState state, String myPubKeyRaw) {
return GetIt.instance<ServiceManager>()
.current
.transactionsParser(txData, state, myPubKeyRaw);
}
Future<PayResult> pay(
{required List<String> to, required double amount, String? comment}) async {
return GetIt.instance<ServiceManager>()
.current
.pay(to: to, amount: amount, comment: comment);
}
Future<String?> getProfileUserName(String pubKey) {
return GetIt.instance<ServiceManager>().current.getProfileUserName(pubKey);
}
Future<bool> createOrUpdateProfile(String name) {
return GetIt.instance<ServiceManager>().current.createOrUpdateProfile(name);
}
Future<bool> deleteProfile() {
return GetIt.instance<ServiceManager>().current.deleteProfile();
}
// From duniter-vue
// https://git.duniter.org/HugoTrentesaux/duniter-vue/-/blob/master/src/distance.ts?ref_type=heads
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import '../data/models/is_json_serializable.dart';
part 'distance_precompute.g.dart';
@JsonSerializable()
class DistancePrecompute extends Equatable
implements IsJsonSerializable<DistancePrecompute> {
const DistancePrecompute({
required this.height,
required this.block,
required this.refereesCount,
required this.memberCount,
required this.minCertsForReferee,
required this.results,
});
factory DistancePrecompute.fromJson(Map<String, dynamic> json) =>
_$DistancePrecomputeFromJson(json);
final int height;
final String block;
final int refereesCount;
final int memberCount;
final int minCertsForReferee;
@JsonKey(fromJson: _mapFromJson, toJson: _mapToJson)
final Map<int, int> results;
static Map<int, int> _mapFromJson(Map<String, dynamic> json) {
return json.map((String key, dynamic value) =>
MapEntry<int, int>(int.parse(key), int.parse(value.toString())));
}
static Map<String, dynamic> _mapToJson(Map<int, int> map) {
return map.map((int key, int value) =>
MapEntry<String, dynamic>(key.toString(), value));
}
@override
DistancePrecompute fromJson(Map<String, dynamic> json) =>
_$DistancePrecomputeFromJson(json);
@override
Map<String, dynamic> toJson() => _$DistancePrecomputeToJson(this);
@override
List<Object> get props => <Object>[
height,
block,
refereesCount,
memberCount,
minCertsForReferee,
results
];
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'distance_precompute.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DistancePrecompute _$DistancePrecomputeFromJson(Map<String, dynamic> json) =>
DistancePrecompute(
height: (json['height'] as num).toInt(),
block: json['block'] as String,
refereesCount: (json['refereesCount'] as num).toInt(),
memberCount: (json['memberCount'] as num).toInt(),
minCertsForReferee: (json['minCertsForReferee'] as num).toInt(),
results: DistancePrecompute._mapFromJson(
json['results'] as Map<String, dynamic>),
);
Map<String, dynamic> _$DistancePrecomputeToJson(DistancePrecompute instance) =>
<String, dynamic>{
'height': instance.height,
'block': instance.block,
'refereesCount': instance.refereesCount,
'memberCount': instance.memberCount,
'minCertsForReferee': instance.minCertsForReferee,
'results': DistancePrecompute._mapToJson(instance.results),
};
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'distance_precompute.dart';
// From duniter-vue
// https://git.duniter.org/HugoTrentesaux/duniter-vue/-/blob/master/src/distance.ts?ref_type=heads
class DistancePrecomputeProvider {
Future<DistancePrecompute?> fetchDistancePrecompute() async {
const String url =
'https://files.coinduf.eu/distance_precompute/latest_distance.json';
try {
final http.Response response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final Map<String, dynamic> jsonData =
json.decode(response.body) as Map<String, dynamic>;
return parse(jsonData);
} else {
debugPrint(
'Failed to fetch distance precompute: ${response.statusCode}');
}
} catch (e) {
debugPrint('Error fetching distance precompute: $e');
}
return null;
}
DistancePrecompute parse(Map<String, dynamic> json) {
final Map<int, int> results = <int, int>{};
for (final MapEntry<String, dynamic> entry
in (json['results'] as Map<String, dynamic>).entries) {
results[int.parse(entry.key)] = int.parse(entry.value.toString());
}
return DistancePrecompute(
height: json['height'] as int,
block: json['block'] as String,
refereesCount: json['referees_count'] as int,
memberCount: json['member_count'] as int,
minCertsForReferee: json['min_certs_for_referee'] as int,
results: results,
);
}
}
import 'dart:convert';
import 'dart:typed_data';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/json_object.dart';
import 'package:duniter_datapod/duniter_datapod_client.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-mutations.data.gql.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-mutations.req.gql.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-mutations.var.gql.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-queries.data.gql.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-queries.req.gql.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod-queries.var.gql.dart';
import 'package:duniter_datapod/graphql/schema/__generated__/duniter-datapod.schema.schema.gql.dart';
import 'package:ferry/ferry.dart' as ferry;
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
import 'package:polkadart/scale_codec.dart';
import 'package:polkadart_keyring/polkadart_keyring.dart';
import '../data/models/contact.dart';
import '../data/models/lat_lng_parse.dart';
import '../data/models/node.dart';
import '../data/models/node_manager.dart';
import '../data/models/node_type.dart';
import '../shared_prefs_helper.dart';
import '../ui/contacts_cache.dart';
import '../ui/logger.dart';
import '../ui/ui_helpers.dart';
import 'api.dart';
import 'duniter_indexer_helper.dart';
import 'g1_v2_helper.dart';
const Duration defDatapodTimeout = Duration(seconds: 20);
Future<T> executeOnDatapodNodes<T>(
Future<T> Function(Node node, ferry.Client client) operation,
{bool retry = true,
Duration timeout = defDatapodTimeout}) async {
final List<Node> nodes = NodeManager().getBestNodes(NodeType.datapodEndpoint);
nodes.shuffle();
for (final Node node in nodes) {
try {
final ferry.Client client =
await initDuniterDatapodClient(node.url, GetIt.instance<HiveStore>());
final T result = await operation(node, client).timeout(timeout);
return result; // If the operation is successful, return the result
} catch (e, stacktrace) {
NodeManager().increaseNodeErrors(NodeType.datapodEndpoint, node);
loggerDev('Error in node ${node.url}', error: e, stackTrace: stacktrace);
if (!retry) {
rethrow;
}
}
}
throw Exception(
'All nodes failed to execute the operation'); // If all nodes fail, throw an exception
}
Future<Uint8List?> fetchAndResizeAvatar(String? avatarCid,
{bool resize = true}) async {
if (avatarCid == null) {
return null;
}
final Uint8List? avatarBase64 = await _getAvatarFromIpfs(avatarCid);
return checkAndResizeAvatar(avatarBase64, resize);
}
/*
{
"data": {
"profiles": [
{
"avatar": null,
"pubkey": "5FiDA1i3Qk6GzQz4r67QygmuAD8LLKSuccXRnznN1yLvG5ns",
"description": null,
"title": "vjrj ❥",
"city": null,
"data_cid": "QmbzrrBCoVy5G8rz9Gw5i7MFzHhF6WmDncKwMsuC3za77k",
"geoloc": null,
"index_request_cid": "bafyreia3dnocpc3xf7r7dk32ogoy57hw5e3ua2vby2zahatyaqox6dorny",
"socials": null,
"time": "2023-09-30T16:58:58"
},
{
"avatar": "QmdUEF24hzEF6SW198sRJ2C1hWd3XFADALKBBgciCooAfC",
"pubkey": "5DpRqhjEof2WMFoAYTAqqoQzyBH9zRRmAbBZGQm9uZSzNeY6",
"description": "Ğ1nkgo author",
"title": "vjrj",
"city": "Spain",
"data_cid": "QmSXeSD9vsVKRrKAUabihsFZhhxsdAdeY2WgppkD1kYhu8",
"geoloc": null,
"index_request_cid": "bafyreiey2zeiuu2u5nhiohd6a4gkj6tvc3zlkgfp4nbqv4234wkdoehahm",
"socials": [
{
"url": "https://twitter.com/vjrj",
"type": "twitter"
}
],
"time": "2023-10-22T22:04:44"
}
]
}
}
*/
Future<Contact> createContactFromProfile(
dynamic profile, {
bool resizeAvatar = true,
}) async {
/* final Uint8List? avatar = await fetchAndResizeAvatar(
(profile as dynamic).avatar as String?,
resize: resizeAvatar); */
List<Map<String, String>>? socials;
final ListJsonObject? socialsJson =
(profile as dynamic).socials as ListJsonObject?;
if (socialsJson != null) {
socials = socialsJson.asList
.map((Object? item) => (item! as Map<dynamic, dynamic>).map(
(dynamic key, dynamic value) =>
MapEntry<String, String>(key.toString(), value.toString()),
))
.toList();
}
final Gtimestamp? timeRaw = (profile as dynamic).time as Gtimestamp?;
final Gpoint? geoLocRaw = (profile as dynamic).geoloc as Gpoint?;
return Contact.withAddress(
name: (profile as dynamic).title as String,
address: (profile as dynamic).pubkey as String,
avatarCid: (profile as dynamic).avatar as String?,
description: (profile as dynamic).description as String?,
dataCid: (profile as dynamic).data_cid as String?,
geoLoc: geoLocRaw != null ? LatLngParsing.parse(geoLocRaw.value) : null,
indexRequestCid: (profile as dynamic).index_request_cid as String?,
socials: socials,
time: timeRaw != null ? DateTime.tryParse(timeRaw.value) : null,
city: (profile as dynamic).city as String?,
);
}
Future<GGetProfileByAddressData_profiles?> _searchProfileByPKV2(
String pubkey) async {
executeOnDatapodNodes((Node node, ferry.Client client) async {
loggerDev('Searching profile in node ${node.url} with address $pubkey');
try {
final GGetProfileByAddressReq request = GGetProfileByAddressReq(
(GGetProfileByAddressReqBuilder b) => b..vars.pubkey = pubkey);
final ferry
.OperationResponse<GGetProfileByAddressData, GGetProfileByAddressVars>
response = await client.request(request).first;
if (response.hasErrors) {
throw Exception(
'Error fetching profile by address: ${response.graphqlErrors}');
}
if (response.data!.profiles.isEmpty) {
loggerDev('No profile found for pubkey $pubkey in node ${node.url}');
return null;
}
loggerDev('Profile found for pubkey $pubkey in node ${node.url}');
return response.data?.profiles.first;
} catch (e) {
logger(
'Error fetching profile in node ${node.url} with address $pubkey ($e)');
}
});
return null;
}
Future<Contact> getProfileV2(String pubKey,
{bool onlyProfile = false,
bool resize = true,
bool complete = false}) async {
loggerDev('Fetching profile v2 for pubkey $pubKey');
final String address = addressFromV1PubkeyFaiSafe(pubKey);
final GGetProfileByAddressData_profiles? profile =
await _searchProfileByPKV2(address);
Contact c;
if (profile != null) {
c = await createContactFromProfile(profile, resizeAvatar: resize);
} else {
c = Contact.withAddress(address: address);
}
if (!onlyProfile) {
final Contact cWot = complete
? await getAccount(address: c.address)
: await getAccountBasic(address: c.address);
c = c.merge(cWot);
}
logger('Contact retrieved in getProfile $c (c+ only $onlyProfile)');
ContactsCache().addContact(c);
return c;
}
Future<List<Contact>> getProfilesV2({required List<String> pubKeys}) async {
loggerDev('Fetching profiles v2 for pubkeys $pubKeys');
final List<Contact> contacts = <Contact>[];
if (pubKeys.isEmpty) {
return contacts;
}
for (final Node node
in NodeManager().getBestNodes(NodeType.datapodEndpoint)) {
try {
final ferry.Client client =
await initDuniterDatapodClient(node.url, GetIt.instance<HiveStore>());
final GGetProfilesByAddressReq request = GGetProfilesByAddressReq(
(GGetProfilesByAddressReqBuilder b) => b..vars.pubkeys.addAll(pubKeys),
);
final ferry.OperationResponse<GGetProfilesByAddressData,
GGetProfilesByAddressVars> response =
await client.request(request).first;
if (response.hasErrors) {
throw Exception('GraphQL Error: ${response.graphqlErrors}');
}
final Iterable<GGetProfilesByAddressData_profiles> profiles =
response.data?.profiles ?? <GGetProfilesByAddressData_profiles>[];
for (final GGetProfilesByAddressData_profiles profile in profiles) {
final Contact contact = await createContactFromProfile(profile);
contacts.add(contact);
}
} catch (e) {
logger('Error fetching profiles in node ${node.url}: $e');
}
loggerDev('Contacts retrieved in getProfiles ${contacts.length}');
return contacts;
}
loggerDev('Contacts not found in getProfiles');
return contacts;
}
Future<Uint8List?> _getAvatarFromIpfs(String? avatar) async {
if (avatar == null) {
return null;
}
final List<Node> nodes = NodeManager().nodesWorkingList(NodeType.ipfsGateway);
for (final Node node in nodes) {
// type https://gyroi.de/ipfs/Qmd...AfC
final String ipfsUrl = '${node.url}/ipfs/$avatar';
try {
final http.Response response = await http.get(Uri.parse(ipfsUrl));
if (response.statusCode == 200 && response.bodyBytes.isNotEmpty) {
return response.bodyBytes;
}
} catch (e) {
loggerDev('Error fetching avatar from $ipfsUrl: $e');
continue;
}
}
return null;
}
Future<List<Contact>> searchProfilesV2({
required String searchTermLower,
required String searchTerm,
required String searchTermCapitalized,
}) async {
final List<Contact> contacts = <Contact>[];
for (final Node node
in NodeManager().getBestNodes(NodeType.datapodEndpoint)) {
loggerDev("Searching profiles in node ${node.url} with term '$searchTerm'");
try {
final ferry.Client client =
await initDuniterDatapodClient(node.url, GetIt.instance<HiveStore>());
final GSearchProfilesReq request =
GSearchProfilesReq((GSearchProfilesReqBuilder b) => b
..vars.searchTermLower = searchTermLower
..vars.searchTerm = searchTerm
..vars.searchTermCapitalized = searchTermCapitalized);
final ferry.OperationResponse<GSearchProfilesData, GSearchProfilesVars>
response = await client.request(request).first;
if (response.hasErrors) {
throw Exception('GraphQL Error: ${response.graphqlErrors}');
}
final Iterable<GSearchProfilesData_profiles> profiles =
response.data?.profiles ?? <GSearchProfilesData_profiles>[];
for (final GSearchProfilesData_profiles profile in profiles) {
contacts.add(await createContactFromProfile(profile));
}
} catch (e) {
logger('Error fetching profiles in node ${node.url}: $e');
}
loggerDev('Contacts found in searchProfiles ${contacts.length}');
return contacts;
}
loggerDev('Contacts not found in searchProfilesV2');
return contacts;
}
Future<bool> createOrUpdateProfileV2(String name) async {
final KeyPair kp = await SharedPreferencesHelper().getKeyPair();
final Map<String, dynamic> message = <String, dynamic>{
'address': kp.address,
'title': name
};
final String hash = calculateHash(jsonEncode(message));
final String signature =
encodeHex(kp.sign(Uint8List.fromList(hash.codeUnits)));
return updateProfileV2(
address: kp.address, hash: hash, signature: signature, title: name);
}
Future<bool> updateProfileV2(
{required String address,
String? avatarBase64,
String? city,
String? description,
GGeolocInputBuilder? geoloc,
ListBuilder<GSocialInput>? socials,
required String hash,
required String signature,
String? title}) async {
for (final Node node
in NodeManager().getBestNodes(NodeType.datapodEndpoint)) {
loggerDev('Updating profile in node ${node.url}');
try {
final ferry.Client client =
await initDuniterDatapodClient(node.url, GetIt.instance<HiveStore>());
final GUpdateProfileReq request =
GUpdateProfileReq((GUpdateProfileReqBuilder b) => b
..vars.address = address
..vars.avatarBase64 = avatarBase64
..vars.city = city
..vars.description = description
..vars.geoloc = geoloc
..vars.hash = hash
..vars.signature = signature
..vars.socials = socials
..vars.title = title);
final ferry.OperationResponse<GUpdateProfileData, GUpdateProfileVars>
response = await client.request(request).first;
if (response.hasErrors) {
if (response.graphqlErrors != null) {
log.e('Error updating profile', error: response.graphqlErrors);
}
if (response.linkException != null) {
log.e('Error updating profile', error: response.linkException);
}
continue;
} else {
loggerDev('Profile updated successfully: ${response.data}');
return true;
}
} catch (e) {
log.e('Error updating profile in node ${node.url}', error: e);
}
}
return false;
}
Future<bool> deleteProfileV2() async {
final KeyPair kp = await SharedPreferencesHelper().getKeyPair();
final Map<String, dynamic> message = <String, dynamic>{'address': kp.address};
final String hash = calculateHash(jsonEncode(message));
final String signature =
encodeHex(kp.sign(Uint8List.fromList(hash.codeUnits)));
for (final Node node
in NodeManager().getBestNodes(NodeType.datapodEndpoint)) {
loggerDev('Updating profile in node ${node.url}');
try {
final GDeleteProfileReq request =
GDeleteProfileReq((GDeleteProfileReqBuilder b) => b
..vars.address = kp.address
..vars.hash = hash
..vars.signature = signature);
final ferry.Client client =
await initDuniterDatapodClient(node.url, GetIt.instance<HiveStore>());
final ferry.OperationResponse<GDeleteProfileData, GDeleteProfileVars>
response = await client.request(request).first;
if (response.hasErrors) {
log.e('Error deleting profile', error: response.graphqlErrors);
return false;
} else {
return true;
}
} catch (e) {
log.e('Error updating profile in node ${node.url}', error: e);
}
}
return false;
}
import 'dart:async';
import 'dart:typed_data';
import 'package:duniter_indexer/duniter_indexer_client.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.data.gql.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.req.gql.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.var.gql.dart';
import 'package:durt/durt.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:ferry/ferry.dart' as ferry;
import 'package:ferry/ferry.dart';
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:get_it/get_it.dart';
import 'package:polkadart/polkadart.dart';
import 'package:polkadart/provider.dart';
import 'package:polkadart_keyring/polkadart_keyring.dart';
import 'package:ss58/ss58.dart';
import 'package:tuple/tuple.dart' as tp;
import '../data/models/contact.dart';
import '../data/models/node.dart';
import '../data/models/node_manager.dart';
import '../data/models/node_type.dart';
import '../generated/gdev/gdev.dart';
import '../generated/gdev/types/frame_system/account_info.dart';
import '../generated/gdev/types/gdev_runtime/runtime_call.dart';
import '../generated/gdev/types/pallet_certification/types/idty_cert_meta.dart';
import '../generated/gdev/types/pallet_identity/types/idty_value.dart';
import '../generated/gdev/types/sp_membership/membership_data.dart';
import '../generated/gdev/types/sp_runtime/multi_signature.dart';
import '../generated/gdev/types/sp_runtime/multiaddress/multi_address.dart';
import '../shared_prefs_helper.dart';
import '../ui/logger.dart';
import 'g1_v2_helper.dart';
import 'node_check_result.dart';
import 'pay_result.dart';
import 'sing_and_send.dart';
const Duration defPolkadotTimeout = Duration(seconds: 20);
Future<NodeCheckResult> testEndPointV2(String node, Duration timeout) async {
final Stopwatch stopwatch = Stopwatch()..start();
final Provider provider = Provider.fromUri(parseNodeUrl(node));
final Gdev polkadot = Gdev(provider);
final int currentBlockNumber = (await polkadot.query.system.number()) - 1;
stopwatch.stop();
final NodeCheckResult nodeCheckResult = NodeCheckResult(
latency: stopwatch.elapsed, currentBlock: currentBlockNumber);
return nodeCheckResult;
}
Uri parseNodeUrl(String url) {
final Uri parsedUri = Uri.parse(url);
return parsedUri;
}
Future<T> executeOnPolkadotNodes<T>(
Future<T> Function(Node node, Provider provider, Gdev polkadot) operation,
{bool retry = true,
Duration timeout = defPolkadotTimeout}) async {
final List<Node> nodes = NodeManager().getBestNodes(NodeType.endpoint);
nodes.shuffle();
for (final Node node in nodes) {
try {
final Provider provider = Provider.fromUri(parseNodeUrl(node.url));
final Gdev polkadot = Gdev(provider);
final T result =
await operation(node, provider, polkadot).timeout(timeout);
return result; // If the operation is successful, return the result
} catch (e, stacktrace) {
NodeManager().increaseNodeErrors(NodeType.endpoint, node);
loggerDev('Error in node ${node.url}', error: e, stackTrace: stacktrace);
if (!retry) {
rethrow;
}
}
}
throw Exception(
'All nodes failed to execute the operation'); // If all nodes fail, throw an exception
}
Future<IdtyValue?> polkadotIdentity(Contact contact) async {
return executeOnPolkadotNodes<IdtyValue?>(
(Node node, Provider provider, Gdev polkadot) async {
if (contact.index == null) {
return null;
}
return polkadot.query.identity.identities(contact.index!);
});
}
Future<int> polkadotCurrentBlock() async {
return executeOnPolkadotNodes<int>(
(Node node, Provider provider, Gdev polkadot) async {
return polkadot.query.system.number();
});
}
Future<IdtyCertMeta?> polkadotIdtyCertMeta(Contact contact) async {
return executeOnPolkadotNodes<IdtyCertMeta?>(
(Node node, Provider provider, Gdev polkadot) async {
if (contact.index == null) {
return null;
}
return polkadot.query.certification.storageIdtyCertMeta(contact.index!);
});
}
Future<MembershipData?> polkadortMembershipData(Contact contact) async {
return executeOnPolkadotNodes<MembershipData?>(
(Node node, Provider provider, Gdev polkadot) async {
if (contact.index == null) {
return null;
}
return polkadot.query.membership.membership(contact.index!);
});
}
Future<BigInt?> getBalanceV2(
{required String address, Duration timeout = defPolkadotTimeout}) async {
return executeOnPolkadotNodes<BigInt?>(
(Node node, Provider provider, Gdev polkadot) async {
final Address account = Address.decode(address);
final Uint8List pubkey = account.pubkey;
final AccountInfo accountInfo =
await polkadot.query.system.account(pubkey).timeout(timeout);
loggerDev(
'Fetching balance for $address in node ${node.url} gives ${accountInfo.data.free}');
return accountInfo.data.free;
});
}
Future<tp.Tuple2<Map<String, dynamic>?, Node>> getHistoryAndBalanceV2(
String pubKeyRaw,
{int? pageSize = 10,
int? from,
int? to,
String? cursor,
required bool isConnected}) async {
final String address = addressFromV1PubkeyFaiSafe(pubKeyRaw);
final BigInt? balance = await getBalanceV2(address: address);
if (balance == null) {
throw Exception('Error fetching balance for $pubKeyRaw/$address');
}
loggerDev('Fetching balance for $pubKeyRaw/$address gives $balance');
for (final Node node in NodeManager().getBestNodes(NodeType.duniterIndexer)) {
try {
// Force for testing
// node = Node(url: 'https://squid.polkadot.coinduf.eu/v1/graphql');
final ferry.Client client =
await initDuniterIndexerClient(node.url, GetIt.instance<HiveStore>());
loggerDev('Fetching history for $pubKeyRaw/$address in node ${node.url}');
final GAccountTransactionsReq request =
GAccountTransactionsReq((GAccountTransactionsReqBuilder b) => b
..fetchPolicy =
isConnected ? FetchPolicy.NetworkOnly : FetchPolicy.CacheFirst
..vars.accountId = address
..vars.limit = pageSize
..vars.offset = int.parse(cursor ?? '0'));
final ferry
.OperationResponse<GAccountTransactionsData, GAccountTransactionsVars>
response = await client.request(request).first;
if (response.hasErrors) {
NodeManager().increaseNodeErrors(NodeType.duniterIndexer, node);
loggerDev(
'Error fetching data: ${response.graphqlErrors} for $pubKeyRaw/$address in node ${node.url}',
error: response.graphqlErrors);
throw Exception('Error fetching data: ${response.graphqlErrors}');
}
final Map<String, dynamic>? data = response.data?.toJson();
if (data != null) {
data['balance'] = balance;
}
loggerDev('Fetched history for $pubKeyRaw/$address in node ${node.url}');
return tp.Tuple2<Map<String, dynamic>?, Node>(data, node);
} catch (e, stackTrace) {
loggerDev(
'Error fetching history for$pubKeyRaw/$address in node ${node.url}',
error: e,
stackTrace: stackTrace);
}
}
return const tp.Tuple2<Map<String, dynamic>?, Node>(null, Node(url: ''));
}
Future<SignAndSendResult> requestDistanceEvaluationFor(int idtyIndex,
{Duration timeout = defPolkadotTimeout}) async {
final CesiumWallet walletV1 = await SharedPreferencesHelper().getWallet();
final KeyPair wallet = KeyPair.ed25519.fromSeed(walletV1.seed);
return executeOnPolkadotNodes<SignAndSendResult>(
(Node node, Provider provider, Gdev polkadot) async {
// distance rule has been evaluated positively locally on web of trust at block storage.distance.evaluationBlock()
// TODO(vjrj): Implement this
// polkadot.query.distance.evaluationBlock();
// Error to show too:
// Distance already in evaluation
final RuntimeCall call =
polkadot.tx.distance.requestDistanceEvaluationFor(target: idtyIndex);
return signAndSend(
node,
provider,
polkadot,
wallet,
call,
messageTransformer: _defaultResultTransformer,
);
});
}
Future<SignAndSendResult> requestDistanceEvaluation(
{Duration timeout = defPolkadotTimeout}) async {
final CesiumWallet walletV1 = await SharedPreferencesHelper().getWallet();
final KeyPair wallet = KeyPair.ed25519.fromSeed(walletV1.seed);
return executeOnPolkadotNodes<SignAndSendResult>(
(Node node, Provider provider, Gdev polkadot) async {
// distance rule has been evaluated positively locally on web of trust at block storage.distance.evaluationBlock()
// TODO(vjrj): Implement this
// polkadot.query.distance.evaluationBlock();
// Error to show too:
// Distance already in evaluation
final RuntimeCall call = polkadot.tx.distance.requestDistanceEvaluation();
return signAndSend(
node,
provider,
polkadot,
wallet,
call,
messageTransformer: _defaultResultTransformer,
);
});
}
Future<SignAndSendResult> createIdentity(
{required Contact you, Duration timeout = defPolkadotTimeout}) async {
final CesiumWallet walletV1 = await SharedPreferencesHelper().getWallet();
final KeyPair wallet = KeyPair.ed25519.fromSeed(walletV1.seed);
return executeOnPolkadotNodes(
(Node node, Provider provider, Gdev polkadot) async {
final RuntimeCall call = polkadot.tx.identity.createIdentity(
ownerKey: Address.decode(you.address).pubkey,
);
return signAndSend(
node,
provider,
polkadot,
wallet,
call,
messageTransformer: _defaultResultTransformer,
);
});
}
Future<SignAndSendResult> confirmIdentity(String identityName,
{Duration timeout = defPolkadotTimeout}) async {
final CesiumWallet walletV1 = await SharedPreferencesHelper().getWallet();
final KeyPair wallet = KeyPair.ed25519.fromSeed(walletV1.seed);
return executeOnPolkadotNodes(
(Node node, Provider provider, Gdev polkadot) async {
final RuntimeCall call =
polkadot.tx.identity.confirmIdentity(idtyName: identityName.codeUnits);
return signAndSend(
node,
provider,
polkadot,
wallet,
call,
messageTransformer: _defaultResultTransformer,
);
});
}
Future<SignAndSendResult> certify(int idtyIndex,
{Duration timeout = defPolkadotTimeout}) async {
final CesiumWallet walletV1 = await SharedPreferencesHelper().getWallet();
final KeyPair wallet = KeyPair.ed25519.fromSeed(walletV1.seed);
return executeOnPolkadotNodes(
(Node node, Provider provider, Gdev polkadot) async {
final RuntimeCall call =
polkadot.tx.certification.addCert(receiver: idtyIndex);
return signAndSend(
node,
provider,
polkadot,
wallet,
call,
messageTransformer: _defaultResultTransformer,
);
});
}
Constants polkadotConstants() {
final Provider provider =
Provider.fromUri(parseNodeUrl(NodeManager().endpointNodes.first.url));
final Gdev polkadot = Gdev(provider);
return polkadot.constant;
}
Future<PayResult> payV2({
required List<String> to,
required double amount,
String? comment,
}) async {
final CesiumWallet walletV1 = await SharedPreferencesHelper().getWallet();
final KeyPair wallet = KeyPair.ed25519.fromSeed(walletV1.seed);
final List<String> addresses = <String>[];
final StreamController<String> progressController =
StreamController<String>();
for (final String dest in to) {
try {
addresses.add(addressFromV1PubkeyFaiSafe(dest));
} catch (e) {
progressController
.add(tr('Error converting pubkey $dest to address: $e'));
progressController.close();
return PayResult(
message: tr('Error converting pubkey $dest to address: $e'),
progressStream: progressController.stream,
);
}
}
return executeOnPolkadotNodes(retry: false,
(Node node, Provider provider, Gdev polkadot) async {
RuntimeCall transferCall;
if (addresses.length > 1 || comment != null) {
final List<RuntimeCall> batchCalls = addresses.map((String address) {
final Id multiAddress =
const $MultiAddress().id(Address.decode(address).pubkey);
return polkadot.tx.balances.transferKeepAlive(
dest: multiAddress,
value: BigInt.from(amount * 100),
);
}).toList();
if (comment != null) {
batchCalls.add(
polkadot.tx.system.remarkWithEvent(remark: comment.codeUnits),
);
}
transferCall = polkadot.tx.utility.batch(calls: batchCalls);
} else {
// No comment, one receiver
final Id multiAddress =
const $MultiAddress().id(Address.decode(addresses.first).pubkey);
transferCall = polkadot.tx.balances.transferKeepAlive(
dest: multiAddress,
value: BigInt.from(amount * 100),
);
}
final SignAndSendResult result = await signAndSend(
node,
provider,
polkadot,
wallet,
transferCall,
messageTransformer: _paymentResultTransformer,
);
return PayResult(
node: result.node,
progressStream: result.progressStream,
message: '',
);
});
}
String _paymentResultTransformer(String statusType) {
return _resultTransformer('tx', statusType, 'payment_successful');
}
String _defaultResultTransformer(String statusType) {
return _resultTransformer('op', statusType, 'op_successful');
}
String _resultTransformer(String suffix, String statusType, String success) {
return <String, String>{
'finalized': tr(success),
'ready': tr('${suffix}_ready'),
'inBlock': tr('${suffix}_in_block'),
'broadcast': tr('${suffix}_broadcast'),
'dropped': tr('${suffix}_dropped'),
'invalid': tr('${suffix}_invalid'),
'usurped': tr('${suffix}_usurped'),
'future': tr('${suffix}_processing'),
}[statusType] ??
tr('${suffix}_processing');
}
Future<SignAndSendResult> renew(int idtyIndex,
{Duration timeout = defPolkadotTimeout}) async {
final KeyPair wallet = KeyPair.ed25519
.fromSeed((await SharedPreferencesHelper().getWallet()).seed);
return executeOnPolkadotNodes(
(Node node, Provider provider, Gdev polkadot) async {
final RuntimeCall call =
polkadot.tx.certification.renewCert(receiver: idtyIndex);
return signAndSend(
node,
provider,
polkadot,
wallet,
call,
messageTransformer: _defaultResultTransformer,
);
});
}
Future<SignAndSendResult> revoke(
int idtyIndex, List<int> revocationKey, MultiSignature revocationSig,
{Duration timeout = defPolkadotTimeout}) async {
final KeyPair wallet = KeyPair.ed25519
.fromSeed((await SharedPreferencesHelper().getWallet()).seed);
return executeOnPolkadotNodes(
(Node node, Provider provider, Gdev polkadot) async {
final RuntimeCall call = polkadot.tx.identity.revokeIdentity(
idtyIndex: idtyIndex,
revocationKey: revocationKey,
revocationSig: revocationSig);
return signAndSend(
node,
provider,
polkadot,
wallet,
call,
messageTransformer: _defaultResultTransformer,
);
});
}
import 'package:built_collection/built_collection.dart';
import 'package:duniter_indexer/duniter_indexer_client.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.data.gql.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.req.gql.dart';
import 'package:duniter_indexer/graphql/schema/__generated__/duniter-indexer-queries.var.gql.dart';
import 'package:ferry/ferry.dart' as ferry;
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:get_it/get_it.dart';
import '../data/models/cert.dart';
import '../data/models/contact.dart';
import '../data/models/identity_status.dart';
import '../data/models/node.dart';
import '../data/models/node_manager.dart';
import '../data/models/node_type.dart';
import '../ui/contacts_cache.dart';
import '../ui/logger.dart';
import 'g1_helper.dart';
import 'g1_v2_helper.dart';
Future<List<Contact>> searchWotV2(String searchPatternRaw) async {
final List<Contact> contacts = <Contact>[];
for (final Node node in NodeManager().getBestNodes(NodeType.duniterIndexer)) {
loggerDev('Searching indexer v2 with pattern $searchPatternRaw');
try {
// if is a v1Key, search pubkey
final String searchPattern = validateKey(searchPatternRaw)
? addressFromV1Pubkey(searchPatternRaw)
: searchPatternRaw;
loggerDev("Searching indexer v2 with '$searchPattern'");
if (searchPattern.length < 8) {
loggerDev('Searching wot by name');
final GIdentitiesByNameReq req = GIdentitiesByNameReq(
(GIdentitiesByNameReqBuilder b) => b..vars.pattern = searchPattern);
// Warn: We are caching results in the hive store
final ferry.Client client = await initDuniterIndexerClient(
node.url, GetIt.instance<HiveStore>());
final ferry
.OperationResponse<GIdentitiesByNameData, GIdentitiesByNameVars>
response = await client.request(req).first;
if (response.hasErrors) {
loggerDev('Error: ${response.linkException?.originalException}');
} else {
final GIdentitiesByNameData? identity = response.data;
for (final GIdentitiesByNameData_identity identity
in identity!.identity) {
final String? address = identity.accountId;
if (address == null) {
loggerDev('ERROR: Pubkey is null');
} else {
contacts.add(
Contact.withAddress(nick: identity.name, address: address));
}
}
}
} else {
loggerDev('Searching wot by name or pk');
final GIdentitiesByNameOrPkReq req =
GIdentitiesByNameOrPkReq((GIdentitiesByNameOrPkReqBuilder b) => b
// Improve this
..fetchPolicy = ferry.FetchPolicy.NetworkOnly
..vars.pattern = searchPattern);
final ferry.Client client = await initDuniterIndexerClient(
node.url, GetIt.instance<HiveStore>());
final ferry.OperationResponse<GIdentitiesByNameOrPkData,
GIdentitiesByNameOrPkVars> response =
await client.request(req).first;
if (response.hasErrors) {
loggerDev('Error: ${response.linkException?.originalException}');
} else {
final GIdentitiesByNameOrPkData? identities = response.data;
for (final GIdentitiesByNameOrPkData_identity identity
in identities!.identity) {
final String? address = identity.accountId;
if (address == null) {
loggerDev('ERROR: Pubkey is null');
} else {
contacts.add(_contactFromIdentity(identity));
}
}
}
}
// If works without errors, break
break;
} catch (e) {
log.e('Error searching wot', error: e);
}
loggerDev('Contacts found in wot search ${contacts.length}');
return contacts;
}
loggerDev('Contacts not found in wot search');
ContactsCache().addContacts(contacts);
return contacts;
}
Future<List<Contact>> getIdentities({required List<String> addresses}) async {
final List<Contact> contacts = <Contact>[];
for (final Node node in NodeManager().getBestNodes(NodeType.duniterIndexer)) {
loggerDev('Searching indexer v2 with pubKeys $addresses');
try {
final GIdentitiesByPkReq req =
GIdentitiesByPkReq((GIdentitiesByPkReqBuilder b) => b
// Improve this
..fetchPolicy = ferry.FetchPolicy.NetworkOnly
..vars.pubKeys.replace(addresses));
final ferry.Client client =
await initDuniterIndexerClient(node.url, GetIt.instance<HiveStore>());
final ferry.OperationResponse<GIdentitiesByPkData, GIdentitiesByPkVars>
response = await client.request(req).first;
if (response.hasErrors) {
loggerDev('Error: ${response.linkException?.originalException}',
error: response.linkException?.originalException);
} else {
final GIdentitiesByPkData? identities = response.data;
for (final GIdentitiesByPkData_identity identity
in identities!.identity) {
final String? address = identity.accountId;
if (address == null) {
loggerDev('ERROR: Pubkey is null');
} else {
contacts.add(_contactFromIdentity(identity));
}
}
}
// If works without errors, break
break;
} catch (e) {
log.e('Error searching wot', error: e);
}
loggerDev('Contacts found in wot search ${contacts.length}');
return contacts;
}
loggerDev('Contacts not found in wot search');
ContactsCache().addContacts(contacts);
return contacts;
}
Contact _contactFromIdentity(dynamic identity) {
if (identity == null) {
throw ArgumentError('Identity cannot be null');
}
List<Cert> certReceived = <Cert>[];
try {
certReceived = ((identity as dynamic).certReceived as BuiltList<dynamic>)
.map((dynamic cert) => _buildCert(cert))
.toList();
} catch (e) {
// Do nothing
}
List<Cert> certIssued = <Cert>[];
try {
certIssued = ((identity as dynamic).certIssued as BuiltList<dynamic>)
.map((dynamic cert) => _buildCert(cert))
.toList();
} catch (e) {
// Do nothing
}
return Contact.withAddress(
nick: (identity as dynamic).name as String?,
address: (identity as dynamic).accountId as String,
certsReceived: certReceived,
certsIssued: certIssued,
status: parseIdentityStatus(
((identity as dynamic).status as dynamic)?.name as String?),
isMember: (identity as dynamic).isMember as bool?,
index: (identity as dynamic).index as int?,
createdOn: ((identity as dynamic).account as dynamic).createdOn as int?,
expireOn: (identity as dynamic).expireOn as int?,
);
}
Cert _buildCert(dynamic cert) {
final dynamic issuer = (cert as dynamic).issuer;
final dynamic receiver = (cert as dynamic).receiver;
return Cert(
id: (cert as dynamic).id as String,
issuerId: Contact.withAddress(
name: (issuer as dynamic).name as String,
createdOn: ((issuer as dynamic).account as dynamic).createdOn as int,
address: (issuer as dynamic).accountId as String,
status: parseIdentityStatus(
((issuer as dynamic)?.status as dynamic)?.name as String?),
isMember: (issuer as dynamic)?.isMember as bool?,
expireOn: (issuer as dynamic).expireOn as int?,
index: (issuer as dynamic).index as int?),
receiverId: Contact.withAddress(
name: (receiver as dynamic).name as String,
createdOn: ((receiver as dynamic).account as dynamic).createdOn as int,
address: (receiver as dynamic).accountId as String,
status: parseIdentityStatus(
((issuer as dynamic)?.status as dynamic)?.name as String?),
isMember: (receiver as dynamic)?.isMember as bool?,
expireOn: (receiver as dynamic).expireOn as int?,
index: (receiver as dynamic).index as int?),
createdOn: (cert as dynamic).createdOn as int,
expireOn: (cert as dynamic).expireOn as int,
isActive: (cert as dynamic).isActive as bool,
updatedOn: (cert as dynamic).updatedOn as int,
);
}
Future<Contact?> getIdentity({required String address}) async {
final List<Contact> contacts =
await getIdentities(addresses: <String>[address]);
return contacts.isNotEmpty ? contacts.first : null;
}
Contact _contactFromAccount(dynamic account) {
if (account == null) {
throw ArgumentError('Account cannot be null');
}
final dynamic identity = (account as dynamic).identity;
List<Cert> certReceived = <Cert>[];
List<Cert> certIssued = <Cert>[];
try {
certReceived = ((identity as dynamic).certReceived as BuiltList<dynamic>)
.map((dynamic cert) => _buildCert(cert))
.toList();
} catch (e) {
// Do nothing
}
try {
certIssued = ((identity as dynamic).certIssued as BuiltList<dynamic>)
.map((dynamic cert) => _buildCert(cert))
.toList();
} catch (e) {
// Do nothing
}
return identity != null
? Contact.withAddress(
nick: (identity as dynamic)?.name as String?,
address: (account as dynamic).id as String,
status: parseIdentityStatus(
((identity as dynamic)?.status as dynamic)?.name as String?),
isMember: (identity as dynamic)?.isMember as bool?,
createdOn: (account as dynamic).createdOn as int?,
expireOn: (identity as dynamic).expireOn as int?,
index: (identity as dynamic).index as int?,
certsIssued: certIssued,
certsReceived: certReceived,
)
: Contact.withAddress(
address: (account as dynamic).id as String,
createdOn: (account as dynamic).createdOn as int?,
);
}
Future<List<Contact>> getAccounts({required List<String> accountIds}) async {
final List<Contact> contacts = <Contact>[];
for (final Node node in NodeManager().getBestNodes(NodeType.duniterIndexer)) {
loggerDev('Fetching accounts with IDs: $accountIds');
try {
final GAccountsByPkReq req = GAccountsByPkReq(
(GAccountsByPkReqBuilder b) => b
..fetchPolicy = ferry.FetchPolicy.NetworkOnly
..vars.accountIds.replace(accountIds),
);
final ferry.Client client =
await initDuniterIndexerClient(node.url, GetIt.instance<HiveStore>());
final ferry.OperationResponse<GAccountsByPkData, GAccountsByPkVars>
response = await client.request(req).first;
if (response.hasErrors) {
loggerDev('Error: ${response.linkException?.originalException}',
error: response.linkException?.originalException);
} else {
final GAccountsByPkData? accountsData = response.data;
if (accountsData != null) {
for (final dynamic account in accountsData.account) {
final Contact contact = _contactFromAccount(account);
contacts.add(contact);
}
}
}
break;
} catch (e) {
log.e('Error fetching accounts', error: e);
// retry
}
}
loggerDev('Accounts found in wot search ${contacts.length}');
ContactsCache().addContacts(contacts);
return contacts;
}
Future<Contact> getAccount({required String address}) async {
final List<Contact> contacts =
await getAccounts(accountIds: <String>[address]);
return contacts.isNotEmpty
? contacts.first
: Contact.withAddress(address: address);
}
Future<List<Contact>> getAccountsBasic(
{required List<String> accountIds}) async {
final List<Contact> contacts = <Contact>[];
for (final Node node in NodeManager().getBestNodes(NodeType.duniterIndexer)) {
loggerDev('Fetching accounts with IDs: $accountIds');
try {
final GAccountsBasicByPkReq req = GAccountsBasicByPkReq(
(GAccountsBasicByPkReqBuilder b) => b
..fetchPolicy = ferry.FetchPolicy.NetworkOnly
..vars.accountIds.replace(accountIds),
);
final ferry.Client client =
await initDuniterIndexerClient(node.url, GetIt.instance<HiveStore>());
final ferry
.OperationResponse<GAccountsBasicByPkData, GAccountsBasicByPkVars>
response = await client.request(req).first;
if (response.hasErrors) {
loggerDev('Error: ${response.linkException?.originalException}',
error: response.linkException?.originalException);
} else {
final GAccountsBasicByPkData? accountsData = response.data;
if (accountsData != null) {
for (final dynamic account in accountsData.account) {
final Contact contact = _contactFromAccount(account);
contacts.add(contact);
}
}
}
break;
} catch (e, st) {
log.e('Error fetching accounts', error: e, stackTrace: st);
// retry
}
}
loggerDev('Contacts found in wot search ${contacts.length}');
loggerDev('Fetched ${contacts.length} accounts');
ContactsCache().addContacts(contacts);
return contacts;
}
Future<Contact> getAccountBasic({required String address}) async {
final List<Contact> contacts =
await getAccountsBasic(accountIds: <String>[address]);
return contacts.isNotEmpty
? contacts.first
: Contact.withAddress(address: address);
}
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:durt/durt.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fast_base58/fast_base58.dart';
import 'package:flutter/material.dart';
import 'package:pointycastle/pointycastle.dart';
import 'g1_helper.dart';
String generateWif(CesiumWallet wallet) {
final Uint8List seed = wallet.seed;
if (seed.length != 32) {
throw ArgumentError('Private key must be 32 bytes long');
}
final List<int> wifData = <int>[0x01];
wifData.addAll(seed);
final List<int> doubleHash =
sha256.convert(sha256.convert(Uint8List.fromList(wifData)).bytes).bytes;
final List<int> checksum = doubleHash.sublist(0, 2);
wifData.addAll(checksum);
return Base58Encode(Uint8List.fromList(wifData));
}
String generateEwif(CesiumWallet wallet, String password) {
final Uint8List seed = wallet.seed;
final Uint8List signPk = Uint8List.fromList(Base58Decode(wallet.pubkey));
final List<int> ewifData = <int>[0x02];
final List<int> salt =
sha256.convert(sha256.convert(signPk).bytes).bytes.sublist(0, 4);
ewifData.addAll(salt);
final ScryptParameters scryptParams =
ScryptParameters(16384, 8, 8, 64, Uint8List.fromList(salt));
final KeyDerivator scrypt = KeyDerivator('scrypt')..init(scryptParams);
final Uint8List scryptSeed =
scrypt.process(Uint8List.fromList(password.codeUnits));
final Uint8List derivedhalf1 = scryptSeed.sublist(0, 32);
final Uint8List derivedhalf2 = scryptSeed.sublist(32, 64);
final Uint8List xorHalf1 = Uint8List(16);
final Uint8List xorHalf2 = Uint8List(16);
for (int i = 0; i < 16; i++) {
xorHalf1[i] = seed[i] ^ derivedhalf1[i];
xorHalf2[i] = seed[i + 16] ^ derivedhalf1[i + 16];
}
final Uint8List encryptedHalf1 = encryptAes(xorHalf1, derivedhalf2);
final Uint8List encryptedHalf2 = encryptAes(xorHalf2, derivedhalf2);
ewifData.addAll(encryptedHalf1);
ewifData.addAll(encryptedHalf2);
final List<int> checksum = sha256
.convert(sha256.convert(Uint8List.fromList(ewifData)).bytes)
.bytes
.sublist(0, 2);
ewifData.addAll(checksum);
return Base58Encode(Uint8List.fromList(ewifData));
}
final String keyFileNamePrefix = tr('wallet_key_prefix');
Map<String, String> generatePubSecFile(String pubKey, String secKey) {
final String fileName = 'g1-$keyFileNamePrefix-$pubKey-PubSec.dunikey';
final String content = '''
Type: PubSec
Version: 1
pub: $pubKey
sec: $secKey
''';
return <String, String>{fileName: content};
}
Map<String, String> generateWifFile(String pubKey, String wifData) {
final String fileName = 'g1-$keyFileNamePrefix-$pubKey-WIF.dunikey';
final String content = '''
Type: WIF
Version: 1
Data: $wifData
''';
return <String, String>{fileName: content};
}
Map<String, String> generateEwifFile(String pubKey, String ewifData) {
final String fileName = 'g1-$keyFileNamePrefix-$pubKey-EWIF.dunikey';
final String content = '''
Type: EWIF
Version: 1
Data: $ewifData
''';
return <String, String>{fileName: content};
}
String getPrivKey(CesiumWallet wallet) {
final Uint8List privKeyComplete =
Uint8List.fromList(wallet.seed + wallet.rootKey.publicKey);
return Base58Encode(privKeyComplete);
}
Future<CesiumWallet> parseKeyFile(String fileContent,
[BuildContext? context, String? password]) async {
final RegExp typeRegExp = RegExp(r'^Type: (\w+)', multiLine: true);
final RegExp pubRegExp = RegExp(r'pub: ([a-zA-Z0-9]+)', multiLine: true);
final RegExp secRegExp = RegExp(r'sec: ([a-zA-Z0-9]+)', multiLine: true);
final RegExp dataRegExp = RegExp(r'Data: ([a-zA-Z0-9]+)', multiLine: true);
final Match? typeMatch = typeRegExp.firstMatch(fileContent);
if (typeMatch == null) {
throw const FormatException('We cannot detect the type of the file.');
}
final String fileType = typeMatch.group(1)!;
switch (fileType) {
case 'PubSec':
return _parsePubSec(fileContent, pubRegExp, secRegExp);
case 'WIF':
return _parseWif(fileContent, dataRegExp);
case 'EWIF':
return password != null && password.isNotEmpty
? _parseEwif(fileContent, password, dataRegExp)
: _promptPasswordForEwif(context!, fileContent, dataRegExp);
default:
throw FormatException('Type $fileType not supported.');
}
}
Future<CesiumWallet> _promptPasswordForEwif(
BuildContext context, String fileContent, RegExp dataRegExp) async {
final TextEditingController passwordController = TextEditingController();
final CesiumWallet? wallet = await showDialog<CesiumWallet>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(tr('enter_password')),
content: TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: tr('password'),
hintText: tr('password_hint'),
),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(tr('cancel')),
),
TextButton(
onPressed: () {
final String password = passwordController.text.trim();
if (password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr('password_empty_error')),
backgroundColor: Colors.red,
),
);
return;
}
try {
final CesiumWallet wallet =
_parseEwif(fileContent, password, dataRegExp);
Navigator.of(context).pop(wallet);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr('ewif_parse_error')),
backgroundColor: Colors.red,
),
);
}
},
child: Text(tr('ok')),
),
],
);
},
);
if (wallet == null) {
throw const FormatException('EWIF file parsing was cancelled.');
}
return wallet;
}
CesiumWallet _parsePubSec(
String fileContent, RegExp pubRegExp, RegExp secRegExp) {
final Match? pubMatch = pubRegExp.firstMatch(fileContent);
final Match? secMatch = secRegExp.firstMatch(fileContent);
if (pubMatch == null || secMatch == null) {
throw const FormatException('Missing data in PubSec file.');
}
final Uint8List privKeyComplete =
Uint8List.fromList(Base58Decode(secMatch.group(1)!));
final Uint8List privKey = privKeyComplete.sublist(0, 32);
return CesiumWallet.fromSeed(privKey);
}
CesiumWallet _parseWif(String fileContent, RegExp dataRegExp) {
final Match? dataMatch = dataRegExp.firstMatch(fileContent);
if (dataMatch == null) {
throw const FormatException('Missing data in WIF file.');
}
final Uint8List wifBytes =
Uint8List.fromList(Base58Decode(dataMatch.group(1)!));
final Uint8List privKey =
wifBytes.sublist(1, 33); // Exclude prefix and checksum
if (privKey.length != 32) {
throw FormatException(
'Private key length is not 32 bytes, found: ${privKey.length}');
}
return CesiumWallet.fromSeed(privKey);
}
CesiumWallet _parseEwif(
String fileContent, String password, RegExp dataRegExp) {
final Match? dataMatch = dataRegExp.firstMatch(fileContent);
if (dataMatch == null) {
throw const FormatException('Wrong data in EWIF file.');
}
final Uint8List ewifBytes =
Uint8List.fromList(Base58Decode(dataMatch.group(1)!));
final Uint8List salt = ewifBytes.sublist(1, 5);
final Uint8List encryptedHalf1 = ewifBytes.sublist(5, 21);
final Uint8List encryptedHalf2 = ewifBytes.sublist(21, 37);
final ScryptParameters scryptParams = ScryptParameters(16384, 8, 8, 64, salt);
final KeyDerivator scrypt = KeyDerivator('scrypt')..init(scryptParams);
final Uint8List scryptSeed =
scrypt.process(Uint8List.fromList(password.codeUnits));
final Uint8List derivedhalf1 = scryptSeed.sublist(0, 32);
final Uint8List derivedhalf2 = scryptSeed.sublist(32, 64);
final Uint8List decryptedHalf1 = decryptAes(encryptedHalf1, derivedhalf2);
final Uint8List decryptedHalf2 = decryptAes(encryptedHalf2, derivedhalf2);
final Uint8List privKey = Uint8List(32);
for (int i = 0; i < 16; i++) {
privKey[i] = decryptedHalf1[i] ^ derivedhalf1[i];
privKey[i + 16] = decryptedHalf2[i] ^ derivedhalf1[i + 16];
}
if (privKey.length != 32) {
throw FormatException(
'Private key length is not 32 bytes, found: ${privKey.length}');
}
return CesiumWallet.fromSeed(privKey);
}
......@@ -2,8 +2,10 @@ import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:crypto/crypto.dart';
import 'package:durt/durt.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:encrypt/encrypt.dart';
import 'package:fast_base58/fast_base58.dart';
......@@ -13,6 +15,7 @@ import '../data/models/contact.dart';
import '../data/models/payment_state.dart';
import '../data/models/transaction.dart';
import '../data/models/utxo.dart';
import '../ui/currency_helper.dart';
import '../ui/logger.dart';
import '../ui/pay_helper.dart';
import '../ui/ui_helpers.dart';
......@@ -158,8 +161,8 @@ String pkChecksum(String pubkey) {
}
// Double SHA256 hash
final Digest firstHash = sha256.convert(signpkInt8);
final Digest secondHash = sha256.convert(firstHash.bytes);
final crypto.Digest firstHash = sha256.convert(signpkInt8);
final crypto.Digest secondHash = sha256.convert(firstHash.bytes);
// Base58 encode and take the first 3 characters
final String checksum = Base58Encode(secondHash.bytes).substring(0, 3);
......@@ -261,7 +264,9 @@ PaymentState? parseScannedUri(String qrOrig) {
return null;
}
final IV _iv = encrypt.IV.fromLength(16);
// https://github.com/leocavalcante/encrypt/issues/314#issuecomment-1729499372
// final IV _iv = encrypt.IV.fromLength(16);
final IV _iv = IV(Uint8List(16));
Map<String, String> encryptJsonForExport(String jsonString, String password) {
final Uint8List plainText = Uint8List.fromList(utf8.encode(jsonString));
......@@ -274,12 +279,23 @@ Map<String, String> encryptJsonForExport(String jsonString, String password) {
return jsonData;
}
Map<String, dynamic> decryptJsonForImport(
String keyEncrypted, String password) {
final String decrypted = encrypt.Encrypter(
encrypt.AES(encrypt.Key.fromUtf8(password.padRight(32))))
.decrypt64(keyEncrypted, iv: _iv);
return jsonDecode(decrypted) as Map<String, dynamic>;
Map<String, dynamic> decryptJsonForImport(String keyEncrypted, String password,
[bool debug = false]) {
// This fails if encrypt > 5.0.1
// https://github.com/leocavalcante/encrypt/issues/314
try {
final Key key = encrypt.Key.fromUtf8(password.padRight(32));
final AES aes = encrypt.AES(key);
final String decrypted =
encrypt.Encrypter(aes).decrypt64(keyEncrypted, iv: _iv);
return jsonDecode(decrypted) as Map<String, dynamic>;
} catch (e, stacktrace) {
if (debug) {
logger('Decrypt error: $e');
logger(stacktrace);
}
rethrow;
}
}
const Duration wrongNodeDuration = Duration(days: 2);
......@@ -314,14 +330,18 @@ final RegExp regex = RegExp(
r'[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}(:([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{3}))?',
);
List<Contact> parseMultipleKeys(String inputText) {
final List<Contact> contacts = <Contact>[];
Set<Contact> parseMultipleKeys(String inputText) {
final Set<Contact> contacts = <Contact>{};
final Iterable<RegExpMatch> allMatches = regex.allMatches(inputText);
loggerDev('matches: ${allMatches.length}');
for (final RegExpMatch match in allMatches) {
final String? publicKey = match.group(0);
if (publicKey != null) {
contacts.add(Contact(pubKey: publicKey));
if (publicKey != null && validateKey(publicKey)) {
try {
contacts.add(Contact(pubKey: publicKey));
} catch (e) {
loggerDev('Error adding v1 $publicKey $e');
}
}
}
return contacts;
......@@ -378,3 +398,62 @@ List<Transaction> lastTx(List<Transaction> origTxs) {
areDatesClose(DateTime.now(), tx.time, paymentTimeRange))
.toList();
}
Uint8List encryptAes(Uint8List data, Uint8List key) {
final encrypt.Encrypter encrypter = encrypt.Encrypter(encrypt.AES(
encrypt.Key(key),
mode: encrypt.AESMode.ecb,
padding: null,
));
final encrypt.Encrypted encrypted =
encrypter.encryptBytes(data, iv: encrypt.IV(Uint8List(16)));
return Uint8List.fromList(encrypted.bytes);
}
Uint8List decryptAes(Uint8List encryptedData, Uint8List key) {
final encrypt.Encrypter encrypter = encrypt.Encrypter(encrypt.AES(
encrypt.Key(key),
mode: encrypt.AESMode.ecb,
padding: null,
));
final List<int> decrypted = encrypter.decryptBytes(
encrypt.Encrypted(encryptedData),
iv: encrypt.IV(Uint8List(16)));
return Uint8List.fromList(decrypted);
}
// Based on duniter-vue
DateTime estimateDateFromBlock(
{required int futureBlock, required int currentBlockHeight}) {
const int millisPerBlock = 6000;
final int diff = futureBlock - currentBlockHeight;
return DateTime.now().add(Duration(milliseconds: diff * millisPerBlock));
}
bool isMe(Contact contact, String publicAddress) =>
extractPublicKey(contact.pubKey) == extractPublicKey(publicAddress);
String humanizePubKey(String rawAddress, [bool minimal = false]) {
final String address = extractPublicKey(rawAddress);
return minimal
? '\u{1F5DD} ${simplifyPubKey(address).substring(0, 4)}'
: '\u{1F5DD} ${simplifyPubKey(address)}';
}
String humanizeAddress(String address, [bool minimal = false]) {
return minimal
? ' \u{1F511} ${simplifyPubKey(address).substring(0, 4)}'
: ' \u{1F511} ${simplifyPubKey(address)}';
}
String simplifyPubKey(String address) => address.length <= 8
? 'WRONG ADDRESS'
: '${address.substring(0, 4)}${address.substring(address.length - 4)}';
String humanizeFromToPubKey(String publicAddress, String address) {
if (address == publicAddress) {
return tr('your_wallet');
} else {
return humanizePubKey(address);
}
}
export 'g1_v2_helper_others.dart'
if (dart.library.html) 'g1_v2_helper_web.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:durt/durt.dart' as durt;
import 'package:fast_base58/fast_base58.dart';
import 'package:polkadart_keyring/polkadart_keyring.dart';
import '../ui/logger.dart';
import 'g1_helper.dart';
// From:
// https://polkadot.js.org/docs/util-crypto/examples/validate-address/
bool isValidV2Address(String address) {
try {
final Keyring keyring = Keyring();
keyring.encodeAddress(
isHex(address) ? hexToU8a(address) : keyring.decodeAddress(address));
return true;
} catch (error) {
return false;
}
}
Uint8List hexToU8a(String hexString) {
hexString = hexString.startsWith('0x') ? hexString.substring(2) : hexString;
if (hexString.length % 2 != 0) {
hexString = '0$hexString';
}
return Uint8List.fromList(List<int>.generate(hexString.length ~/ 2, (int i) {
return int.parse(hexString.substring(i * 2, i * 2 + 2), radix: 16);
}));
}
bool isHex(String value, [int bitLength = -1]) {
final RegExp hexRegEx = RegExp(r'^0x[a-fA-F0-9]+$');
return hexRegEx.hasMatch(value) &&
(bitLength == -1 || value.length == 2 + bitLength ~/ 4);
}
String addressFromV1Pubkey(String pubkey) {
final Keyring keyring = Keyring();
final List<int> pubkeyByte = Base58Decode(pubkey);
final String address = keyring.encodeAddress(pubkeyByte);
return address;
}
String v1pubkeyFromAddress(String address) {
final Keyring keyring = Keyring();
final Uint8List publicKeyBytes = keyring.decodeAddress(address);
final String publicKey = Base58Encode(publicKeyBytes);
return publicKey;
}
Keyring keyringFromV1Seed(Uint8List seed) {
final Keyring keyring = Keyring();
final KeyPair keypair = KeyPair.ed25519.fromSeed(seed);
keyring.add(keypair);
return keyring;
}
Keyring keyringFromSeed(Uint8List seed) {
final Keyring keyring = Keyring();
final KeyPair keypair = KeyPair.sr25519.fromSeed(seed);
keyring.add(keypair);
return keyring;
}
// From durt
String mnemonicGenerate({String lang = 'english'}) {
final List<String> supportedLanguages = <String>[
'english',
'french',
'italian',
'spanish'
];
if (!supportedLanguages.contains(lang)) {
throw ArgumentError('Unsupported language');
}
final String mnemonic = durt.generateMnemonic(lang: lang);
return mnemonic;
}
// From:
// https://polkadot.js.org/docs/keyring/start/create
Future<KeyPair> addPair() async {
final String mnemonic = mnemonicGenerate();
final Keyring keyring = Keyring();
// create & add the pair to the keyring with the type
// TODOAdd some additional metadata as in polkadot-js
final KeyPair pair =
await keyring.fromUri(mnemonic, keyPairType: KeyPairType.sr25519);
return pair;
}
String addressFromV1PubkeyFaiSafe(String pubKeyRaw) {
try {
return addressFromV1Pubkey(extractPublicKey(pubKeyRaw));
} catch (e) {
loggerDev('Error converting pubkey $pubKeyRaw to address: $e');
rethrow;
}
}