CW-491-Send-to-Mastodon-username-addresses (#1107)

* Send to Mastodon username addresses * Update mastodon_user.dart * Enhance Eth out of gas error condition Remove some code warnings [skip ci] --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

Serhii committed Oct 5, 2023 at 15:18 UTC b41489321148465a6a506e2b71dcbde863ea1456
7 files changed +162 -15
lib/di.dart
+12 -12
@@ -217,7 +217,6 @@ import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
217 import 'package:cake_wallet/core/wallet_loading_service.dart';
218 import 'package:cw_core/crypto_currency.dart';
219 import 'package:cake_wallet/entities/qr_view_data.dart';
220 -import 'package:cake_wallet/nano/nano.dart' as nanoNano;
220
221 import 'core/totp_request_details.dart';
222
@@ -644,7 +643,7 @@ Future<void> setup({
643 return MoneroAccountListViewModel(wallet);
644 }
645 throw Exception(
647 - 'Unexpected wallet type: ${wallet.type} for generate Nano/Monero AccountListViewModel');
646 + 'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel');
647 });
648
649 getIt.registerFactory(
@@ -929,7 +928,7 @@ Future<void> setup({
928 wallet: wallet!);
929 });
930
932 - getIt.registerFactoryParam<BuyWebViewPage, List, void>((List args, _) {
931 + getIt.registerFactoryParam<BuyWebViewPage, List<dynamic>, void>((List<dynamic> args, _) {
932 final url = args.first as String;
933 final buyViewModel = args[1] as BuyViewModel;
934
@@ -958,7 +957,7 @@ Future<void> setup({
957 getIt.registerFactory(() {
958 final wallet = getIt.get<AppStore>().wallet;
959
961 - return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource!);
960 + return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource);
961 });
962
963 getIt.registerFactory(() =>
@@ -969,7 +968,7 @@ Future<void> setup({
968 (item, model) =>
969 UnspentCoinsDetailsViewModel(unspentCoinsItem: item, unspentCoinsListViewModel: model));
970
972 - getIt.registerFactoryParam<UnspentCoinsDetailsPage, List, void>((List args, _) {
971 + getIt.registerFactoryParam<UnspentCoinsDetailsPage, List<dynamic>, void>((List<dynamic> args, _) {
972 final item = args.first as UnspentCoinsItem;
973 final unspentCoinsListViewModel = args[1] as UnspentCoinsListViewModel;
974
@@ -1022,7 +1021,7 @@ Future<void> setup({
1021
1022 getIt.registerFactory(() => IoniaLoginPage(getIt.get<IoniaAuthViewModel>()));
1023
1025 - getIt.registerFactoryParam<IoniaVerifyIoniaOtp, List, void>((List args, _) {
1024 + getIt.registerFactoryParam<IoniaVerifyIoniaOtp, List<dynamic>, void>((List<dynamic> args, _) {
1025 final email = args.first as String;
1026 final isSignIn = args[1] as bool;
1027
@@ -1031,13 +1030,14 @@ Future<void> setup({
1030
1031 getIt.registerFactory(() => IoniaWelcomePage());
1032
1034 - getIt.registerFactoryParam<IoniaBuyGiftCardPage, List, void>((List args, _) {
1033 + getIt.registerFactoryParam<IoniaBuyGiftCardPage, List<dynamic>, void>((List<dynamic> args, _) {
1034 final merchant = args.first as IoniaMerchant;
1035
1036 return IoniaBuyGiftCardPage(getIt.get<IoniaBuyCardViewModel>(param1: merchant));
1037 });
1038
1040 - getIt.registerFactoryParam<IoniaBuyGiftCardDetailPage, List, void>((List args, _) {
1039 + getIt.registerFactoryParam<IoniaBuyGiftCardDetailPage, List<dynamic>, void>(
1040 + (List<dynamic> args, _) {
1041 final amount = args.first as double;
1042 final merchant = args.last as IoniaMerchant;
1043 return IoniaBuyGiftCardDetailPage(
@@ -1050,7 +1050,7 @@ Future<void> setup({
1050 ioniaService: getIt.get<IoniaService>(), giftCard: giftCard);
1051 });
1052
1053 - getIt.registerFactoryParam<IoniaCustomTipViewModel, List, void>((List args, _) {
1053 + getIt.registerFactoryParam<IoniaCustomTipViewModel, List<dynamic>, void>((List<dynamic> args, _) {
1054 final amount = args[0] as double;
1055 final merchant = args[1] as IoniaMerchant;
1056 final tip = args[2] as IoniaTip;
@@ -1063,7 +1063,7 @@ Future<void> setup({
1063 return IoniaGiftCardDetailPage(getIt.get<IoniaGiftCardDetailsViewModel>(param1: giftCard));
1064 });
1065
1066 - getIt.registerFactoryParam<IoniaMoreOptionsPage, List, void>((List args, _) {
1066 + getIt.registerFactoryParam<IoniaMoreOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
1067 final giftCard = args.first as IoniaGiftCard;
1068
1069 return IoniaMoreOptionsPage(giftCard);
@@ -1073,13 +1073,13 @@ Future<void> setup({
1073 (IoniaGiftCard giftCard, _) =>
1074 IoniaCustomRedeemViewModel(giftCard: giftCard, ioniaService: getIt.get<IoniaService>()));
1075
1076 - getIt.registerFactoryParam<IoniaCustomRedeemPage, List, void>((List args, _) {
1076 + getIt.registerFactoryParam<IoniaCustomRedeemPage, List<dynamic>, void>((List<dynamic> args, _) {
1077 final giftCard = args.first as IoniaGiftCard;
1078
1079 return IoniaCustomRedeemPage(getIt.get<IoniaCustomRedeemViewModel>(param1: giftCard));
1080 });
1081
1082 - getIt.registerFactoryParam<IoniaCustomTipPage, List, void>((List args, _) {
1082 + getIt.registerFactoryParam<IoniaCustomTipPage, List<dynamic>, void>((List<dynamic> args, _) {
1083 return IoniaCustomTipPage(getIt.get<IoniaCustomTipViewModel>(param1: args));
1084 });
1085
lib/entities/parse_address_from_domain.dart
+35
@@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/openalias_record.dart';
5 import 'package:cake_wallet/entities/parsed_address.dart';
6 import 'package:cake_wallet/entities/unstoppable_domain_address.dart';
7 import 'package:cake_wallet/entities/emoji_string_extension.dart';
8 +import 'package:cake_wallet/mastodon/mastodon_api.dart';
9 import 'package:cake_wallet/twitter/twitter_api.dart';
10 import 'package:cw_core/crypto_currency.dart';
11 import 'package:cw_core/wallet_base.dart';
@@ -73,6 +74,40 @@ class AddressResolver {
74 }
75 }
76 }
77 +
78 + if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) {
79 + final subText = text.substring(1);
80 + final hostNameIndex = subText.indexOf('@');
81 + final hostName = subText.substring(hostNameIndex + 1);
82 + final userName = subText.substring(0, hostNameIndex);
83 +
84 + final mastodonUser =
85 + await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName);
86 +
87 + if (mastodonUser != null) {
88 + String? addressFromBio =
89 + extractAddressByType(raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker));
90 +
91 + if (addressFromBio != null) {
92 + return ParsedAddress.fetchMastodonAddress(address: addressFromBio, name: text);
93 + } else {
94 + final pinnedPosts =
95 + await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName);
96 +
97 + if (pinnedPosts.isNotEmpty) {
98 + final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n');
99 + String? addressFromPinnedPost = extractAddressByType(
100 + raw: userPinnedPostsText, type: CryptoCurrency.fromString(ticker));
101 +
102 + if (addressFromPinnedPost != null) {
103 + return ParsedAddress.fetchMastodonAddress(
104 + address: addressFromPinnedPost, name: text);
105 + }
106 + }
107 + }
108 + }
109 + }
110 +
111 if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) {
112 final bool isFioRegistered = await FioAddressProvider.checkAvail(text);
113 if (isFioRegistered) {
lib/entities/parsed_address.dart
+10 -1
@@ -1,7 +1,8 @@
1 import 'package:cake_wallet/entities/openalias_record.dart';
2 import 'package:cake_wallet/entities/yat_record.dart';
3
4 -enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, ens, contact }
4 +
5 +enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, ens, contact, mastodon }
6
7 class ParsedAddress {
8 ParsedAddress({
@@ -69,6 +70,14 @@ class ParsedAddress {
70 );
71 }
72
73 + factory ParsedAddress.fetchMastodonAddress({required String address, required String name}){
74 + return ParsedAddress(
75 + addresses: [address],
76 + name: name,
77 + parseFrom: ParseFrom.mastodon
78 + );
79 + }
80 +
81 factory ParsedAddress.fetchContactAddress({required String address, required String name}){
82 return ParsedAddress(
83 addresses: [address],
lib/mastodon/mastodon_api.dart new
+63
@@ -0,0 +1,63 @@
1 +import 'dart:convert';
2 +import 'package:http/http.dart' as http;
3 +import 'package:cake_wallet/mastodon/mastodon_user.dart';
4 +
5 +class MastodonAPI {
6 + static const httpsScheme = 'https';
7 + static const userPath = '/api/v1/accounts/lookup';
8 + static const statusesPath = '/api/v1/accounts/:id/statuses';
9 +
10 + static Future<MastodonUser?> lookupUserByUserName(
11 + {required String userName, required String apiHost}) async {
12 + try {
13 + final queryParams = {'acct': userName};
14 +
15 + final uri = Uri(
16 + scheme: httpsScheme,
17 + host: apiHost,
18 + path: userPath,
19 + queryParameters: queryParams,
20 + );
21 +
22 + final response = await http.get(uri);
23 +
24 + if (response.statusCode != 200) return null;
25 +
26 + final Map<String, dynamic> responseJSON = json.decode(response.body) as Map<String, dynamic>;
27 +
28 + return MastodonUser.fromJson(responseJSON);
29 + } catch (e) {
30 + print('Error in lookupUserByUserName: $e');
31 + return null;
32 + }
33 + }
34 +
35 + static Future<List<PinnedPost>> getPinnedPosts({
36 + required String userId,
37 + required String apiHost,
38 + }) async {
39 + try {
40 + final queryParams = {'pinned': 'true'};
41 +
42 + final uri = Uri(
43 + scheme: httpsScheme,
44 + host: apiHost,
45 + path: statusesPath.replaceAll(':id', userId),
46 + queryParameters: queryParams,
47 + );
48 +
49 + final response = await http.get(uri);
50 +
51 + if (response.statusCode != 200) {
52 + throw Exception('Unexpected HTTP status: ${response.statusCode}');
53 + }
54 +
55 + final List<dynamic> responseJSON = json.decode(response.body) as List<dynamic>;
56 +
57 + return responseJSON.map((json) => PinnedPost.fromJson(json as Map<String, dynamic>)).toList();
58 + } catch (e) {
59 + print('Error in getPinnedPosts: $e');
60 + throw e;
61 + }
62 + }
63 +}
lib/mastodon/mastodon_user.dart new
+36
@@ -0,0 +1,36 @@
1 +class MastodonUser {
2 + String id;
3 + String username;
4 + String acct;
5 + String note;
6 +
7 + MastodonUser({
8 + required this.id,
9 + required this.username,
10 + required this.acct,
11 + required this.note,
12 + });
13 +
14 + factory MastodonUser.fromJson(Map<String, dynamic> json) {
15 + return MastodonUser(
16 + id: json['id'] as String,
17 + username: json['username'] as String,
18 + acct: json['acct'] as String,
19 + note: json['note'] as String,
20 + );
21 + }
22 +}
23 +
24 +class PinnedPost {
25 + final String id;
26 + final String content;
27 +
28 + PinnedPost({required this.id, required this.content});
29 +
30 + factory PinnedPost.fromJson(Map<String, dynamic> json) {
31 + return PinnedPost(
32 + id: json['id'] as String,
33 + content: json['content'] as String,
34 + );
35 + }
36 +}
lib/src/screens/send/widgets/extract_address_from_parsed.dart
+5
@@ -38,6 +38,11 @@ Future<String> extractAddressFromParsed(
38 content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)');
39 address = parsedAddress.addresses.first;
40 break;
41 + case ParseFrom.mastodon:
42 + title = S.of(context).address_detected;
43 + content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)');
44 + address = parsedAddress.addresses.first;
45 + break;
46 case ParseFrom.yatRecord:
47 if (parsedAddress.name.isEmpty) {
48 title = S.of(context).yat_error;
lib/view_model/send/send_view_model.dart
+1 -2
@@ -1,4 +1,3 @@
1 -import 'package:cake_wallet/di.dart';
1 import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
2 import 'package:cake_wallet/entities/transaction_description.dart';
3 import 'package:cake_wallet/ethereum/ethereum.dart';
@@ -430,7 +429,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
429
430 String translateErrorMessage(String error, WalletType walletType, CryptoCurrency currency,) {
431 if (walletType == WalletType.ethereum || walletType == WalletType.haven) {
433 - if (error.contains('gas required exceeds allowance') || error.contains('insufficient funds for gas')) {
432 + if (error.contains('gas required exceeds allowance') || error.contains('insufficient funds for')) {
433 return S.current.do_not_have_enough_gas_asset(currency.toString());
434 }
435 }