diff --git a/lib/di.dart b/lib/di.dart index aec07ff3f..b572c4b98 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -217,7 +217,6 @@ import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; -import 'package:cake_wallet/nano/nano.dart' as nanoNano; import 'core/totp_request_details.dart'; @@ -644,7 +643,7 @@ Future setup({ return MoneroAccountListViewModel(wallet); } throw Exception( - 'Unexpected wallet type: ${wallet.type} for generate Nano/Monero AccountListViewModel'); + 'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel'); }); getIt.registerFactory( @@ -929,7 +928,7 @@ Future setup({ wallet: wallet!); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final url = args.first as String; final buyViewModel = args[1] as BuyViewModel; @@ -958,7 +957,7 @@ Future setup({ getIt.registerFactory(() { final wallet = getIt.get().wallet; - return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource!); + return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource); }); getIt.registerFactory(() => @@ -969,7 +968,7 @@ Future setup({ (item, model) => UnspentCoinsDetailsViewModel(unspentCoinsItem: item, unspentCoinsListViewModel: model)); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final item = args.first as UnspentCoinsItem; final unspentCoinsListViewModel = args[1] as UnspentCoinsListViewModel; @@ -1022,7 +1021,7 @@ Future setup({ getIt.registerFactory(() => IoniaLoginPage(getIt.get())); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final email = args.first as String; final isSignIn = args[1] as bool; @@ -1031,13 +1030,14 @@ Future setup({ getIt.registerFactory(() => IoniaWelcomePage()); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final merchant = args.first as IoniaMerchant; return IoniaBuyGiftCardPage(getIt.get(param1: merchant)); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>( + (List args, _) { final amount = args.first as double; final merchant = args.last as IoniaMerchant; return IoniaBuyGiftCardDetailPage( @@ -1050,7 +1050,7 @@ Future setup({ ioniaService: getIt.get(), giftCard: giftCard); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final amount = args[0] as double; final merchant = args[1] as IoniaMerchant; final tip = args[2] as IoniaTip; @@ -1063,7 +1063,7 @@ Future setup({ return IoniaGiftCardDetailPage(getIt.get(param1: giftCard)); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final giftCard = args.first as IoniaGiftCard; return IoniaMoreOptionsPage(giftCard); @@ -1073,13 +1073,13 @@ Future setup({ (IoniaGiftCard giftCard, _) => IoniaCustomRedeemViewModel(giftCard: giftCard, ioniaService: getIt.get())); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final giftCard = args.first as IoniaGiftCard; return IoniaCustomRedeemPage(getIt.get(param1: giftCard)); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { return IoniaCustomTipPage(getIt.get(param1: args)); }); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 515b9a2df..14bfe2f7f 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; import 'package:cake_wallet/entities/emoji_string_extension.dart'; +import 'package:cake_wallet/mastodon/mastodon_api.dart'; import 'package:cake_wallet/twitter/twitter_api.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; @@ -73,6 +74,40 @@ class AddressResolver { } } } + + if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) { + final subText = text.substring(1); + final hostNameIndex = subText.indexOf('@'); + final hostName = subText.substring(hostNameIndex + 1); + final userName = subText.substring(0, hostNameIndex); + + final mastodonUser = + await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName); + + if (mastodonUser != null) { + String? addressFromBio = + extractAddressByType(raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker)); + + if (addressFromBio != null) { + return ParsedAddress.fetchMastodonAddress(address: addressFromBio, name: text); + } else { + final pinnedPosts = + await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName); + + if (pinnedPosts.isNotEmpty) { + final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n'); + String? addressFromPinnedPost = extractAddressByType( + raw: userPinnedPostsText, type: CryptoCurrency.fromString(ticker)); + + if (addressFromPinnedPost != null) { + return ParsedAddress.fetchMastodonAddress( + address: addressFromPinnedPost, name: text); + } + } + } + } + } + if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) { final bool isFioRegistered = await FioAddressProvider.checkAvail(text); if (isFioRegistered) { diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index b8c0a81d5..df20dd9ee 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -1,7 +1,8 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/yat_record.dart'; -enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, ens, contact } + +enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, ens, contact, mastodon } class ParsedAddress { ParsedAddress({ @@ -69,6 +70,14 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchMastodonAddress({required String address, required String name}){ + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.mastodon + ); + } + factory ParsedAddress.fetchContactAddress({required String address, required String name}){ return ParsedAddress( addresses: [address], diff --git a/lib/mastodon/mastodon_api.dart b/lib/mastodon/mastodon_api.dart new file mode 100644 index 000000000..8326ce05d --- /dev/null +++ b/lib/mastodon/mastodon_api.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:cake_wallet/mastodon/mastodon_user.dart'; + +class MastodonAPI { + static const httpsScheme = 'https'; + static const userPath = '/api/v1/accounts/lookup'; + static const statusesPath = '/api/v1/accounts/:id/statuses'; + + static Future lookupUserByUserName( + {required String userName, required String apiHost}) async { + try { + final queryParams = {'acct': userName}; + + final uri = Uri( + scheme: httpsScheme, + host: apiHost, + path: userPath, + queryParameters: queryParams, + ); + + final response = await http.get(uri); + + if (response.statusCode != 200) return null; + + final Map responseJSON = json.decode(response.body) as Map; + + return MastodonUser.fromJson(responseJSON); + } catch (e) { + print('Error in lookupUserByUserName: $e'); + return null; + } + } + + static Future> getPinnedPosts({ + required String userId, + required String apiHost, + }) async { + try { + final queryParams = {'pinned': 'true'}; + + final uri = Uri( + scheme: httpsScheme, + host: apiHost, + path: statusesPath.replaceAll(':id', userId), + queryParameters: queryParams, + ); + + final response = await http.get(uri); + + if (response.statusCode != 200) { + throw Exception('Unexpected HTTP status: ${response.statusCode}'); + } + + final List responseJSON = json.decode(response.body) as List; + + return responseJSON.map((json) => PinnedPost.fromJson(json as Map)).toList(); + } catch (e) { + print('Error in getPinnedPosts: $e'); + throw e; + } + } +} diff --git a/lib/mastodon/mastodon_user.dart b/lib/mastodon/mastodon_user.dart new file mode 100644 index 000000000..f5a29f298 --- /dev/null +++ b/lib/mastodon/mastodon_user.dart @@ -0,0 +1,36 @@ +class MastodonUser { + String id; + String username; + String acct; + String note; + + MastodonUser({ + required this.id, + required this.username, + required this.acct, + required this.note, + }); + + factory MastodonUser.fromJson(Map json) { + return MastodonUser( + id: json['id'] as String, + username: json['username'] as String, + acct: json['acct'] as String, + note: json['note'] as String, + ); + } +} + +class PinnedPost { + final String id; + final String content; + + PinnedPost({required this.id, required this.content}); + + factory PinnedPost.fromJson(Map json) { + return PinnedPost( + id: json['id'] as String, + content: json['content'] as String, + ); + } +} diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index 2d0847158..73bff23c1 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -38,6 +38,11 @@ Future extractAddressFromParsed( content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)'); address = parsedAddress.addresses.first; break; + case ParseFrom.mastodon: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index a3457d4af..faebed11f 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -430,7 +429,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor String translateErrorMessage(String error, WalletType walletType, CryptoCurrency currency,) { if (walletType == WalletType.ethereum || walletType == WalletType.haven) { - if (error.contains('gas required exceeds allowance') || error.contains('insufficient funds for gas')) { + if (error.contains('gas required exceeds allowance') || error.contains('insufficient funds for')) { return S.current.do_not_have_enough_gas_asset(currency.toString()); } }