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
}