diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart index 5442dcff1..8380897b9 100644 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart @@ -11,8 +11,8 @@ import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart'; import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart'; import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart'; import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; import 'package:cake_wallet/routes.dart'; @@ -584,8 +584,11 @@ class CakePayBuyCardPage extends BasePage { final displayingOutputs = _sendViewModel.outputs .map((o) => o.OutputCopyWithParsedAddress( parsedAddress: ParsedAddress( - addresses: [o.address], - name: 'Cake Pay', + parsedAddressByCurrencyMap: { + cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency: + o.address, + }, + handle: 'Cake Pay', profileName: order?.cards.first.cardName ?? 'Cake Pay', profileImageUrl: order?.cards.first.cardImagePath ?? '', ), @@ -653,8 +656,11 @@ class CakePayBuyCardPage extends BasePage { final displayingOutputs = outputsCopy .map((o) => o.OutputCopyWithParsedAddress( parsedAddress: ParsedAddress( - addresses: [o.address], - name: 'Cake Pay', + parsedAddressByCurrencyMap: { + cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency: + o.address, + }, + handle: 'Cake Pay', profileName: order?.cards.first.cardName ?? 'Cake Pay', profileImageUrl: order?.cards.first.cardImagePath ?? '', ), diff --git a/lib/core/address_resolver/address_lookup_provider.dart b/lib/core/address_resolver/address_lookup_provider.dart new file mode 100644 index 000000000..9feff5610 --- /dev/null +++ b/lib/core/address_resolver/address_lookup_provider.dart @@ -0,0 +1,22 @@ +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; + +abstract class AddressLookupProvider { + + AddressSource get source; + + List get supportedCurrencies; + + bool isEnabled(SettingsStore settingsStore); + + bool canHandle(String query); + + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }); +} diff --git a/lib/core/address_resolver/address_resolver_service.dart b/lib/core/address_resolver/address_resolver_service.dart new file mode 100644 index 000000000..8592c53c6 --- /dev/null +++ b/lib/core/address_resolver/address_resolver_service.dart @@ -0,0 +1,143 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/bip_353/bip_353_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/ens/ens_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/fio/fio_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurl_pay_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/nostr/nostr_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/openalias/openalias_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_resolver/thorchain/thorchain_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/twitter/twitter_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/wellknown/wellknown_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/yat/yat_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/zano/zano_alias_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/zcash/zcash_address_provider.dart'; +import 'package:cake_wallet/core/address_resolver/zcash/zcash_me_address_provider.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class AddressResolverService { + AddressResolverService({required this.settingsStore}); + + final SettingsStore settingsStore; + +// Sources available for address resolution and lookup settings. + static const supportedSources = [ + AddressSource.lnurlPay, + AddressSource.twitter, + AddressSource.mastodon, + AddressSource.yatRecord, + AddressSource.unstoppableDomains, + AddressSource.openAlias, + AddressSource.ens, + AddressSource.zcashName, + AddressSource.zcashAddress, + AddressSource.wellKnown, + AddressSource.zanoAlias, + AddressSource.bip353, + AddressSource.fio, + AddressSource.thorChain, + AddressSource.nostr, + ]; + + List _buildProviders() { + final allProviders = [ + LNUrlPayAddressProvider(), + TwitterAddressProvider(), + MastodonAddressProvider(), + YatAddressProvider(), + UnstoppableAddressProvider(), + OpenaliasAddressProvider(), + EnsAddressProvider(), + ZcashNameAddressProvider(), + ZcashMeAddressProvider(), + WellKnownAddressProvider(), + ZanoAliasAddressProvider(), + Bip353AddressProvider(), + FioAddressProvider(), + ThorchainAddressProvider(), + NostrAddressProvider(), + ]; + + return allProviders.where((provider) => supportedSources.contains(provider.source)).toList(); + } + + Future> resolve({ + required String query, + required WalletBase wallet, + required CryptoCurrency currency, + }) async { + try { + final providers = _buildProviders(); + // return first entry if currency is specified and provider supports it + if (currency != null) { + for (final provider in providers) { + final cur = currency.title; + final src = provider.source.label; + + printV('[address resolver service] checking $src - $cur - $query'); + if (!provider.isEnabled(settingsStore)) { + printV('[address resolver service] ...skipping $src (disabled)'); + continue; + } + if (!provider.canHandle(query)) { + printV('[address resolver service] ...skipping $src (cannot handle)'); + continue; + } + if (!provider.supportedCurrencies.contains(currency)) { + printV('[address resolver service] ...skipping $src (unsupported currency)'); + continue; + } + + try { + printV('[address resolver service] ...resolving $src - $cur - $query'); + final results = await provider.resolve( + query: query, + currencies: [currency], + wallet: wallet, + ); + + if (results.isNotEmpty) { + final parsedAddress = results.first.parsedAddressByCurrencyMap[currency]; + final parsedAddressStr = parsedAddress == null || parsedAddress.isEmpty ? 'N/A' : parsedAddress; + printV('[address resolver service] resolved $src - $cur - $parsedAddressStr'); + return [results.first]; + } + } catch (e) { + printV('[address resolver service] Error resolving $src: $e'); + } + } + + return []; + } + + return []; // TODO: Implement multi-currency resolution if no specific currency is provided. + + final tasks = >>[]; + + for (final provider in providers) { + if (!provider.isEnabled(settingsStore)) continue; + if (!provider.canHandle(query)) continue; + + tasks.add( + provider.resolve( + query: query, + currencies: provider.supportedCurrencies, + wallet: wallet, + ), + ); + } + + final results = await Future.wait(tasks); + return results.expand((items) => items).toList(); + } catch (e) { + printV('Error resolving address: $e'); + return []; + } + } +} diff --git a/lib/core/address_resolver/address_resolver_utils.dart b/lib/core/address_resolver/address_resolver_utils.dart new file mode 100644 index 000000000..f9f59240c --- /dev/null +++ b/lib/core/address_resolver/address_resolver_utils.dart @@ -0,0 +1,104 @@ +import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; + +class AddressResolverUtils { + static Future> resolveUnstoppableDomainFromText({ + required String raw, + required List currencies, + }) async { + final domain = extractUnstoppableDomain(raw); + if (domain.isEmpty) return {}; + + final result = {}; + + for (final currency in currencies) { + final address = await UnstoppableAddressProvider.fetchUnstoppableDomainAddress(domain, currency.title); + if (address.isNotEmpty) { + result[currency] = address; + } + } + + return result; + } + + static String extractUnstoppableDomain(String raw) { +// sort by length to avoid matching shorter tld instead of longer + final domains = List.from(UnstoppableAddressProvider.unstoppableDomains) + ..sort((a, b) => b.length.compareTo(a.length)); + for (final tld in domains) { + final pattern = RegExp(r'([A-Za-z0-9-]+)\.' + RegExp.escape(tld), caseSensitive: false); + final match = pattern.firstMatch(raw); + if (match != null) return match.group(0)!; + } + return ''; + } + + static Map extractAddressesFromText({ + required String raw, + required List currencies, + bool requireSurroundingWhitespaces = true, + }) { + final result = {}; + var text = raw; + + for (final currency in currencies) { + final address = extractAddressByType( + raw: text, + type: currency, + requireSurroundingWhitespaces: requireSurroundingWhitespaces, + ); + + if (address != null && address.isNotEmpty) { + result[currency] = address; + text = text.replaceFirst(address, ''); + } + } + + return result; + } + + static String? extractAddressByType({ + required String raw, + required CryptoCurrency type, + bool requireSurroundingWhitespaces = true, + }) { + var addressPattern = AddressValidator.getAddressFromStringPattern(type); + if (addressPattern == null) { + printV('Unknown pattern for $type'); + return null; + } + if (requireSurroundingWhitespaces) addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX"; + final text = stripHtmlTags(raw); + final match = RegExp(addressPattern, multiLine: true, caseSensitive: false).firstMatch(text); + if (match == null) return null; + return match.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), + (Match match) { + String group = match.group(0)!; + if (group.startsWith('bitcoincash:') || + group.startsWith('nano_') || + group.startsWith('ban_')) { + return group; + } + return ''; + }); + } + + static bool isEmailFormat(String address) { + final RegExp emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + caseSensitive: false, + ); + return emailRegex.hasMatch(address); + } + + static String stripHtmlTags(String value) { + return value + .replaceAll(RegExp(r'[\u2028\u2029]'), '\n') + .replaceAll(RegExp(r'', caseSensitive: false), '\n') + .replaceAll(RegExp(r'', caseSensitive: false), '\n') + .replaceAll(RegExp(r'<[^>]*>'), ' ') + .trim(); + } +} diff --git a/lib/core/address_resolver/address_sources.dart b/lib/core/address_resolver/address_sources.dart new file mode 100644 index 000000000..729ef478a --- /dev/null +++ b/lib/core/address_resolver/address_sources.dart @@ -0,0 +1,87 @@ +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cw_core/crypto_currency.dart'; + +enum AddressSource { + twitter(label: 'X', iconPath: 'assets/new-ui/address_sources/x.svg', alias: '@username'), + unstoppableDomains( + label: 'Unstoppable Domains', + iconPath: 'assets/new-ui/address_sources/unstoppable.svg', + alias: 'domain.tld'), + openAlias( + label: 'OpenAlias', + iconPath: 'assets/new-ui/address_sources/openalias.svg', + alias: 'name.domain.tld'), + yatRecord(label: 'Yat', iconPath: 'assets/new-ui/address_sources/yat.svg', alias: '🎂🎂🎂'), + fio(label: 'FIO', iconPath: 'assets/new-ui/address_sources/fio.svg', alias: 'user@domain'), + ens( + label: 'Ethereum Name Service', + iconPath: 'assets/new-ui/address_sources/ens.svg', + alias: 'domain.eth'), + mastodon( + label: 'Mastodon', + iconPath: 'assets/new-ui/address_sources/mastodon.svg', + alias: '@username@domain.tld'), + nostr( + label: 'Nostr', + iconPath: 'assets/new-ui/address_sources/nostr.svg', + alias: 'user@domain.tld'), + thorChain( + label: 'ThorChain', + iconPath: 'assets/new-ui/address_sources/icon-thorchain.svg', + alias: 'name'), + wellKnown( + label: '.wellknown', + iconPath: 'assets/new-ui/address_sources/wellknown.svg', + alias: 'domain.tld'), + zanoAlias( + label: 'Zano Alias', iconPath: 'assets/new-ui/address_sources/zano.svg', alias: '@alias'), + bip353( + label: 'BIP353', + iconPath: 'assets/new-ui/address_sources/bip353.svg', + alias: 'user@domain.com'), + zcashAddress( + label: 'Zcash.me', + iconPath: 'assets/new-ui/address_sources/icon-zcash-me.svg', + alias: 'zcash.me/username'), + zcashName( + label: 'Zcash Names', + iconPath: 'assets/new-ui/address_sources/icon-zcashnames.svg', + alias: 'name.zec'), + lnurlPay( + label: 'LNURL Pay', + iconPath: 'assets/new-ui/address_sources/icon-lnurl-pay.svg', + alias: 'user@domain.com'), + contact(label: 'Contact', iconPath: ''), + notParsed(label: 'Unknown', iconPath: ''), + ; + + const AddressSource({ + required this.label, + required this.iconPath, + this.alias = '', + }); + + final String label; + final String iconPath; + final String alias; +} + +extension AddressSourceIndex on AddressSource { + int get raw => index; + + static AddressSource fromRaw(int raw) => + AddressSource.values[raw.clamp(0, AddressSource.values.length - 1)]; +} + +extension AddressSourceNameParser on AddressSource { + static AddressSource fromLabel(String? text) { + if (text == null || text.trim().isEmpty) { + return AddressSource.notParsed; + } + final needle = text.trim().toLowerCase(); + return AddressSource.values.firstWhere( + (src) => src.label.toLowerCase() == needle, + orElse: () => AddressSource.notParsed, + ); + } +} diff --git a/lib/core/address_resolver/bip_353/bip_353_address_provider.dart b/lib/core/address_resolver/bip_353/bip_353_address_provider.dart new file mode 100644 index 000000000..3a46f87a4 --- /dev/null +++ b/lib/core/address_resolver/bip_353/bip_353_address_provider.dart @@ -0,0 +1,68 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/bip_353/bip_353_record.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class Bip353AddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.bip353; + + @override + List get supportedCurrencies => [CryptoCurrency.btc]; + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsBip353; + + @override + bool canHandle(String query) { + final value = query.trim(); + return value.contains('@') && value.contains('.') && !value.startsWith('@'); + } + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final result = {}; + String? dnsProof; + + for (final cur in currencies) { + final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(query, cur.title); + if (bip353AddressMap == null || bip353AddressMap.isEmpty) continue; + + final spAddress = bip353AddressMap['sp']; + final address = bip353AddressMap['address']; + final chosenAddress = spAddress?.isNotEmpty == true ? spAddress : address; + + if (chosenAddress != null && chosenAddress.isNotEmpty) { + result[cur] = chosenAddress; + } + } + + if (result.isEmpty) return []; + + try { + dnsProof = await Bip353Record.fetchDnsProof(query); + } catch (e) { + printV('Bip353Record.fetchDnsProof error: $e'); + } + + return [ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.bip353, + handle: query, + bip353DnsProof: dnsProof ?? '', + )]; + } catch (e) { + printV('[address resolver] Error resolving BIP353 address: $e'); + return []; + } + } +} \ No newline at end of file diff --git a/lib/entities/bip_353_record.dart b/lib/core/address_resolver/bip_353/bip_353_record.dart similarity index 59% rename from lib/entities/bip_353_record.dart rename to lib/core/address_resolver/bip_353/bip_353_record.dart index 9a2a50e0a..257d76737 100644 --- a/lib/entities/bip_353_record.dart +++ b/lib/core/address_resolver/bip_353/bip_353_record.dart @@ -2,13 +2,9 @@ import 'dart:convert'; import 'dart:isolate'; import 'package:basic_utils/basic_utils.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:dnssec_proof/dnssec_proof.dart'; -import 'package:flutter/material.dart'; class Bip353Record { Bip353Record({ @@ -104,69 +100,4 @@ class Bip353Record { } } - static Future pickBip353AddressChoice( - BuildContext context, - String bip353Name, - Map addressMap, - ) async { - if (addressMap.length == 1) { - return addressMap.values.first; - } - - final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap); - - return chosenAddress; - } - - static Future _showAddressChoiceDialog( - BuildContext context, - String bip353Name, - Map addressMap, - ) async { - final entriesList = addressMap.entries.toList(); - final List> displayItems = entriesList.map((entry) { - final originalKey = entry.key; - final originalValue = entry.value; - - final extendedKeyName = keyDisplayMap[originalKey] ?? originalKey; - final truncatedValue = _truncate(originalValue, front: 6, back: 6); - - return { - 'displayKey': extendedKeyName, - 'displayValue': truncatedValue, - 'originalKey': originalKey, - 'originalValue': originalValue, - }; - }).toList(); - - String? selectedOriginalValue; - - if (context.mounted) { - await showPopUp( - context: context, - builder: (dialogContext) { - return AlertWithPickerOption( - alertTitle: S.of(context).multiple_addresses_detected + '\n$bip353Name', - alertTitleTextSize: 14, - alertSubtitle: S.of(context).please_choose_one + ':', - options: displayItems, - onOptionSelected: (Map chosenItem) { - selectedOriginalValue = chosenItem['originalValue']; - }, - alertBarrierDismissible: true, - ); - }, - ); - } - return selectedOriginalValue; - } - - static String _truncate(String value, {int front = 6, int back = 6}) { - if (value.length <= front + back) return value; - - final start = value.substring(0, front); - final end = value.substring(value.length - back); - return '$start...$end'; - } - -} +} \ No newline at end of file diff --git a/lib/core/address_resolver/ens/ens_address_provider.dart b/lib/core/address_resolver/ens/ens_address_provider.dart new file mode 100644 index 000000000..b0a365d07 --- /dev/null +++ b/lib/core/address_resolver/ens/ens_address_provider.dart @@ -0,0 +1,49 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/ens/ens_record.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; + +class EnsAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.ens; + + @override + List get supportedCurrencies => + [CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth]; + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsENS; + + @override + bool canHandle(String query) => query.endsWith('.eth'); // ENS handle example: name.eth + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + final Map result = {}; + + for (final cur in currencies) { + final address = await EnsRecord.fetchEnsAddress(query, cur, wallet: wallet); + if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") { + result[cur] = address; + } + } + + if (result.isNotEmpty) { + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.ens, + handle: query, + ) + ]; + } + return []; + } +} diff --git a/lib/core/address_resolver/ens/ens_record.dart b/lib/core/address_resolver/ens/ens_record.dart new file mode 100644 index 000000000..b976ae63c --- /dev/null +++ b/lib/core/address_resolver/ens/ens_record.dart @@ -0,0 +1,55 @@ +import 'package:cake_wallet/evm/evm.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:ens_dart/ens_dart.dart'; +import 'package:web3dart/web3dart.dart'; + +class EnsRecord { + static Future fetchEnsAddress( + String name, + CryptoCurrency cur, { + WalletBase? wallet, + }) async { + Web3Client? _client; + + if (wallet?.type == WalletType.ethereum || wallet?.type == WalletType.polygon) { + _client = evm!.getWeb3Client(wallet!); + } + + _client ??= Web3Client( + 'https://ethereum-rpc.publicnode.com', + ProxyWrapper().getHttpIOClient(), + ); + + final ens = Ens(client: _client); + final coinType = getEnsCoinType(cur); + + if (coinType == null) { + printV('Unsupported currency for ENS: $cur'); + return ''; + } + + try { + if (coinType == CoinType.ETH || coinType == CoinType.MATIC) { + return (await ens.withName(name).getAddress()).hex; + } else { + return await ens.withName(name).getCoinAddress(coinType); + } + } catch (e) { + printV(e); + return ''; + } + } + + static CoinType? getEnsCoinType(CryptoCurrency cur) => switch (cur) { + CryptoCurrency.xmr => CoinType.XMR, + CryptoCurrency.btc => CoinType.BTC, + CryptoCurrency.ltc => CoinType.LTC, + CryptoCurrency.eth => CoinType.ETH, + CryptoCurrency.matic => CoinType.MATIC, + _ => null, + }; +} \ No newline at end of file diff --git a/lib/core/address_resolver/fio/fio_address_provider.dart b/lib/core/address_resolver/fio/fio_address_provider.dart new file mode 100644 index 000000000..91d4ac722 --- /dev/null +++ b/lib/core/address_resolver/fio/fio_address_provider.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; + +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/wallet_base.dart'; + +class FioAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.fio; + + @override + List get supportedCurrencies => AddressValidator.reliableValidateCurrencies; + + @override + bool canHandle(String q) => + !q.startsWith('@') && + q.contains('@') && + !q.contains('.'); // FIO handle example: username@domain + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsFio; + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + final Map result = {}; + try { + final bool isFioRegistered = await FioAddressProvider.checkAvail(query); + if (!isFioRegistered) return []; + + for (final cur in currencies) { + final address = await FioAddressProvider.getPubAddress(query, cur.title); + if (address != null && address.isNotEmpty) { + result[cur] = address; + } + } + + if (result.isNotEmpty) { + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, addressSource: AddressSource.fio, handle: query) + ]; + } + return []; + } catch (e) { + printV('FioAddressProvider.resolve error: $e'); + return []; + } + } + + static const _apiAuthority = 'fio.blockpane.com'; + static const _availCheck = '/v1/chain/avail_check'; + static const _getAddress = '/v1/chain/get_pub_address'; + + static Future checkAvail(String fioAddress) async { + bool isFioRegistered = false; + final headers = {'Content-Type': 'application/json'}; + final body = {"fio_name": fioAddress}; + + final uri = Uri.https(_apiAuthority, _availCheck); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + + if (response.statusCode != 200) { + return isFioRegistered; + } + + final responseJSON = json.decode(response.body) as Map; + isFioRegistered = responseJSON['is_registered'] as int == 1; + + return isFioRegistered; + } + + static Future getPubAddress(String fioAddress, String token) async { + final headers = {'Content-Type': 'application/json'}; + final body = { + "fio_address": fioAddress, + "chain_code": token.toUpperCase(), + "token_code": token.toUpperCase(), + }; + + final uri = Uri.https(_apiAuthority, _getAddress); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + + final responseJSON = json.decode(response.body) as Map; + + if (response.statusCode == 400) { + final error = responseJSON['error'] as String; + final message = responseJSON['message'] as String; + printV('${error}\n$message'); + return null; + } + + if (response.statusCode != 200) { + final String message = responseJSON['message'] as String? ?? 'Unknown error'; + + printV('Error fetching public address for token $token: $message'); + return null; + } + + final String pubAddress = responseJSON['public_address'] as String? ?? ''; + + if (pubAddress.isNotEmpty) { + return pubAddress; + } + + return null; + } +} diff --git a/lib/core/address_resolver/lnurl_pay/lnurl_pay_address_provider.dart b/lib/core/address_resolver/lnurl_pay/lnurl_pay_address_provider.dart new file mode 100644 index 000000000..b767c15b9 --- /dev/null +++ b/lib/core/address_resolver/lnurl_pay/lnurl_pay_address_provider.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurlpay_record.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; + +class LNUrlPayAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.lnurlPay; + + @override + List get supportedCurrencies => [CryptoCurrency.btcln]; + + @override + bool canHandle(String q) => q.contains('.') && q.contains('@'); // LNURL-pay handle example: user@domain.com + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsLNUrl; + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + if (wallet.type != WalletType.bitcoin) return []; + + final formattedName = query.trim(); + if (formattedName.isEmpty) return []; + + final result = {}; + + for (final currency in currencies) { + if (!supportedCurrencies.contains(currency)) continue; + + final record = await LNUrlPayRecord.fetchAddressAndName( + formattedName: formattedName, + currency: currency, + ); + + if (record != null && record.address.isNotEmpty) { + result[currency] = record.address; + } + } + + if (result.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.lnurlPay, + handle: formattedName, + profileName: formattedName, + ), + ]; + } catch (e) { + printV('[address resolver] Error resolving LNURL-pay address: $e'); + return []; + } + } +} \ No newline at end of file diff --git a/lib/entities/lnurlpay_record.dart b/lib/core/address_resolver/lnurl_pay/lnurlpay_record.dart similarity index 100% rename from lib/entities/lnurlpay_record.dart rename to lib/core/address_resolver/lnurl_pay/lnurlpay_record.dart diff --git a/lib/core/address_resolver/mastodon/mastodon_address_provider.dart b/lib/core/address_resolver/mastodon/mastodon_address_provider.dart new file mode 100644 index 000000000..10c226d0e --- /dev/null +++ b/lib/core/address_resolver/mastodon/mastodon_address_provider.dart @@ -0,0 +1,93 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_api.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class MastodonAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.mastodon; + + @override + List get supportedCurrencies => AddressValidator.reliableValidateCurrencies; + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsMastodon; + + @override + bool canHandle(String query) => + query.startsWith('@') && + query.contains('@', 1) && + query.contains('.', 1); // Mastodon handle example: @username@hostname.xxx + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final subText = query.substring(1); // Remove '@' from the beginning of the handle + final hostNameIndex = subText.indexOf('@'); + if (hostNameIndex <= 0 || hostNameIndex == subText.length - 1) return []; + + final userName = subText.substring(0, hostNameIndex); + final hostName = subText.substring(hostNameIndex + 1); + + final result = {}; + + final mastodonUser = await MastodonAPI.lookupUserByUserName( + userName: userName, + apiHost: hostName, + ); + if (mastodonUser == null) return []; + + final bio = AddressResolverUtils.stripHtmlTags(mastodonUser.note); + result.addAll( + AddressResolverUtils.extractAddressesFromText( + raw: bio, + currencies: currencies, + ), + ); + + final pinnedPosts = await MastodonAPI.getPinnedPosts( + userId: mastodonUser.id, + apiHost: hostName, + ); + + if (pinnedPosts.isNotEmpty) { + final pinnedPostsText = pinnedPosts + .map((item) => AddressResolverUtils.stripHtmlTags(item.content)) + .where((text) => text.isNotEmpty) + .join('\n'); + + result.addAll( + AddressResolverUtils.extractAddressesFromText( + raw: pinnedPostsText, + currencies: currencies, + ), + ); + } + + if (result.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.mastodon, + handle: query, + profileImageUrl: mastodonUser.profileImageUrl, + profileName: mastodonUser.username, + ), + ]; + } catch (e) { + printV('[address resolver] Error resolving Mastodon address: $e'); + return []; + } + } +} diff --git a/lib/mastodon/mastodon_api.dart b/lib/core/address_resolver/mastodon/mastodon_api.dart similarity index 95% rename from lib/mastodon/mastodon_api.dart rename to lib/core/address_resolver/mastodon/mastodon_api.dart index 58767eea7..6cb60be81 100644 --- a/lib/mastodon/mastodon_api.dart +++ b/lib/core/address_resolver/mastodon/mastodon_api.dart @@ -1,7 +1,7 @@ import 'dart:convert'; +import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_user.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cake_wallet/mastodon/mastodon_user.dart'; class MastodonAPI { static const httpsScheme = 'https'; diff --git a/lib/mastodon/mastodon_user.dart b/lib/core/address_resolver/mastodon/mastodon_user.dart similarity index 100% rename from lib/mastodon/mastodon_user.dart rename to lib/core/address_resolver/mastodon/mastodon_user.dart diff --git a/lib/core/address_resolver/nostr/nostr_address_provider.dart b/lib/core/address_resolver/nostr/nostr_address_provider.dart new file mode 100644 index 000000000..332187e9e --- /dev/null +++ b/lib/core/address_resolver/nostr/nostr_address_provider.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/nostr/nostr_api.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class NostrAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.nostr; + + @override + List get supportedCurrencies => AddressValidator.reliableValidateCurrencies; + + @override + bool canHandle(String q) => + AddressResolverUtils.isEmailFormat(q); // Nostr handle example: username@domain + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsNostr; + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final profile = await NostrProfileHandler.queryProfile(query); + if (profile == null) return []; + + final data = await NostrProfileHandler.processRelays(profile, query); + if (data == null) return []; + + final result = {}; + + String queryTxt = data.about; + + for (final cur in currencies) { + final addr = AddressResolverUtils.extractAddressByType(raw: queryTxt, type: cur); + if (addr != null && addr.isNotEmpty) { + result[cur] = addr; + queryTxt = queryTxt.replaceFirst(addr, ''); + } + } + if (result.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.nostr, + handle: query, + profileImageUrl: data.picture, + profileName: data.name, + ) + ]; + } catch (e) { + printV('Error looking up Nostr profile: $e'); + return []; + } + } +} diff --git a/lib/core/address_resolver/nostr/nostr_api.dart b/lib/core/address_resolver/nostr/nostr_api.dart new file mode 100644 index 000000000..19d25c01f --- /dev/null +++ b/lib/core/address_resolver/nostr/nostr_api.dart @@ -0,0 +1,127 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cake_wallet/core/address_resolver/nostr/nostr_user.dart'; +import 'package:nostr_tools/nostr_tools.dart'; +import 'dart:async' show Completer, TimeoutException, runZonedGuarded; + +class NostrProfileHandler { + static final relayToDomainMap = { + 'relay.snort.social': 'snort.social', + }; + + static final Nip05 _nip05 = Nip05(); + + static Future queryProfile(String nip05Address) async { + final profile = await _nip05.queryProfile(nip05Address); + if (profile?.pubkey != null && profile?.relays?.isNotEmpty == true) { + return profile; + } + return null; + } + + static Future processRelays( + ProfilePointer profile, + String nip05Address, + ) async { + final userDomain = _extractDomain(nip05Address); + const int metaKind = 0; + + // Domain-matched relays first + for (final String relayUrl in profile.relays ?? []) { + final relayDomain = + relayToDomainMap[_getDomainFromRelayUrl(relayUrl)] ?? _getDomainFromRelayUrl(relayUrl); + + if (relayDomain == userDomain) { + final data = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaKind]); + if (data != null) return data; + } + } + + // Then try every remaining relay + for (final String relayUrl in profile.relays ?? []) { + final data = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaKind]); + if (data != null) return data; + } + + // Nothing found + return null; + } + + static const Duration _relayTimeout = Duration(seconds: 3); + + static Future _fetchInfoFromRelay( + String relayUrl, String userPubKey, List kinds) async { + // sanitize so obvious junk (like '#') doesn't reach connect() + final clean = _sanitizeRelay(relayUrl); + if (clean.isEmpty) return null; + + final result = Completer(); + + runZonedGuarded(() async { + try { + final relay = RelayApi(relayUrl: clean); + + final stream = await relay.connect().timeout( + _relayTimeout, + onTimeout: () { + relay.close(); + throw TimeoutException('Relay connect timeout'); + }, + ); + + relay.sub([ + Filter(kinds: kinds, authors: [userPubKey]) + ]); + + final sub = stream.listen((msg) { + if (msg.type == 'EVENT' && !result.isCompleted) { + try { + final event = msg.message as Event; + final jsonMap = json.decode(event.content) as Map; + result.complete(UserMetadata.fromJson(jsonMap)); + } catch (_) { + if (!result.isCompleted) result.complete(null); + } + } + }, onError: (_) { + if (!result.isCompleted) result.complete(null); + }, onDone: () { + if (!result.isCompleted) result.complete(null); + }); + + final value = await result.future.timeout(_relayTimeout, onTimeout: () => null); + await sub.cancel(); + relay.close(); + if (!result.isCompleted) result.complete(value); + } catch (_) { + if (!result.isCompleted) result.complete(null); + } + }, (error, stack) { + // swallow ALL async errors from the websocket layer (including "was not upgraded to websocket") + if (!result.isCompleted) result.complete(null); + }); + + return result.future; + } + + static String _sanitizeRelay(String url) { + url = url.replaceFirst(RegExp(r'^https?://'), 'wss://'); + final uri = Uri.parse(url); + return Uri( + scheme: uri.scheme.isEmpty ? 'wss' : uri.scheme, + host: uri.host, + port: uri.hasPort ? uri.port : 443, + ).toString(); + } + + static String _extractDomain(String nip05) => nip05.split('@').last; + + static String _getDomainFromRelayUrl(String url) { + try { + return Uri.parse(url).host; + } catch (_) { + return ''; + } + } +} \ No newline at end of file diff --git a/lib/nostr/nostr_user.dart b/lib/core/address_resolver/nostr/nostr_user.dart similarity index 100% rename from lib/nostr/nostr_user.dart rename to lib/core/address_resolver/nostr/nostr_user.dart diff --git a/lib/core/address_resolver/openalias/openalias_address_provider.dart b/lib/core/address_resolver/openalias/openalias_address_provider.dart new file mode 100644 index 000000000..6f7e8671b --- /dev/null +++ b/lib/core/address_resolver/openalias/openalias_address_provider.dart @@ -0,0 +1,64 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/openalias/openalias_record.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class OpenaliasAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.openAlias; + + @override + List get supportedCurrencies => [CryptoCurrency.xmr, CryptoCurrency.btc]; + + @override + bool canHandle(String q) { + final formattedName = OpenaliasRecord.formatDomainName(q); + return formattedName.contains("."); + } + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsOpenAlias; + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final formatted = OpenaliasRecord.formatDomainName(query); + + final txtRecords = await OpenaliasRecord.lookupOpenAliasRecord(formatted); + if (txtRecords == null) return []; + + final result = {}; + + for (final cur in currencies) { + final rec = OpenaliasRecord.fetchAddressAndName( + formattedName: formatted, + ticker: cur.title.toLowerCase(), + txtRecord: txtRecords, + ); + + if (rec.address.isNotEmpty) result[cur] = rec.address; + } + + if (result.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.openAlias, + handle: query, + ) + ]; + } catch (e) { + printV('[address resolver] Error resolving OpenAlias address: $e'); + return []; + } + } +} diff --git a/lib/core/address_resolver/openalias/openalias_record.dart b/lib/core/address_resolver/openalias/openalias_record.dart new file mode 100644 index 000000000..65273df3e --- /dev/null +++ b/lib/core/address_resolver/openalias/openalias_record.dart @@ -0,0 +1,69 @@ +import 'package:basic_utils/basic_utils.dart'; +import 'package:cw_core/utils/print_verbose.dart'; + +class OpenaliasRecord { + OpenaliasRecord({ + required this.address, + required this.name, + required this.description, + }); + + final String name; + final String address; + final String description; + + static String formatDomainName(String name) { + String formattedName = name; + + if (name.contains("@")) { + formattedName = name.replaceAll("@", "."); + } + + return formattedName; + } + + static Future?> lookupOpenAliasRecord(String name) async { + try { + final txtRecord = await DnsUtils.lookupRecord(name, RRecordType.TXT, dnssec: true); + + return txtRecord; + } catch (e) { + printV("${e.toString()}"); + return null; + } + } + + static OpenaliasRecord fetchAddressAndName({ + required String formattedName, + required String ticker, + required List txtRecord, + }) { + var address = ''; + var name = formattedName; + var note = ''; + + final addrRe = RegExp(r'recipient_address=([^;\s]+)', caseSensitive: false); + final nameRe = RegExp(r'recipient_name=([^;]+)', caseSensitive: false); + final noteRe = RegExp(r'tx_description=([^;]+)', caseSensitive: false); + final tag = 'oa1:$ticker'; + + for (final rr in txtRecord) { + final txt = rr.data.replaceAll('"', ''); + + if (!txt.toLowerCase().contains(tag)) continue; + + final addrM = addrRe.firstMatch(txt); + if (addrM != null) address = addrM.group(1)!; + + final nameM = nameRe.firstMatch(txt); + if (nameM != null) name = nameM.group(1)!.trim(); + + final noteM = noteRe.firstMatch(txt); + if (noteM != null) note = noteM.group(1)!.trim(); + + break; + } + + return OpenaliasRecord(address: address, name: name, description: note); + } +} \ No newline at end of file diff --git a/lib/core/address_resolver/parsed_address.dart b/lib/core/address_resolver/parsed_address.dart new file mode 100644 index 000000000..8934e2d76 --- /dev/null +++ b/lib/core/address_resolver/parsed_address.dart @@ -0,0 +1,22 @@ +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cw_core/crypto_currency.dart'; + +class ParsedAddress { + const ParsedAddress({ + required this.parsedAddressByCurrencyMap, + this.addressSource = AddressSource.notParsed, + this.handle = '', + this.profileImageUrl = '', + this.profileName = '', + this.description = '', + this.bip353DnsProof, + }); + + final Map parsedAddressByCurrencyMap; + final AddressSource addressSource; + final String handle; + final String profileImageUrl; + final String profileName; + final String description; + final String? bip353DnsProof; +} \ No newline at end of file diff --git a/lib/core/address_resolver/thorchain/thorchain_address_provider.dart b/lib/core/address_resolver/thorchain/thorchain_address_provider.dart new file mode 100644 index 000000000..21c27217a --- /dev/null +++ b/lib/core/address_resolver/thorchain/thorchain_address_provider.dart @@ -0,0 +1,65 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class ThorchainAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.thorChain; + + @override + List get supportedCurrencies => AddressValidator.reliableValidateCurrencies; + + @override + bool canHandle(String q) => q.isNotEmpty; // can be any string + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsThorChain; + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final isNormalAddress = currencies.any((cur) => + AddressResolverUtils.extractAddressByType(raw: query, type: cur)?.isNotEmpty ?? false); + if (query.length > 30 || isNormalAddress) return []; + + final map = await ThorChainExchangeProvider.lookupAddressByName(query); + if (map == null || map.isEmpty) return []; + + final result = {}; + + for (final cur in currencies) { + final key = cur.title.toUpperCase(); + final addr = map[key]; + final runeAddr = cur.title.toUpperCase() == 'RUNE' ? map['THOR'] : null; + final resolvedAddress = addr ?? runeAddr; + if (resolvedAddress != null && resolvedAddress.isNotEmpty) { + if (!result.containsValue(resolvedAddress)) result[cur] = resolvedAddress; + } + } + + if (result.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.thorChain, + handle: query, + ) + ]; + } catch (e) { + printV('[address resolver] Error resolving address: $e'); + return []; + } + } +} diff --git a/lib/core/address_resolver/twitter/twitter_address_provider.dart b/lib/core/address_resolver/twitter/twitter_address_provider.dart new file mode 100644 index 000000000..954bfb6ec --- /dev/null +++ b/lib/core/address_resolver/twitter/twitter_address_provider.dart @@ -0,0 +1,97 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_resolver/twitter/twitter_api.dart'; +import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class TwitterAddressProvider extends AddressLookupProvider { + + @override + AddressSource get source => AddressSource.twitter; + + @override + List get supportedCurrencies => AddressValidator.reliableValidateCurrencies; + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsTwitter; + + @override + bool canHandle(String query) => + query.startsWith('@') && !query.substring(1).contains('@'); // x handle example: @username + + /// Resolves a Twitter handle to a list of ParsedAddress objects by extracting potential cryptocurrency addresses + /// from the user's, profile. + /// fallback to resolving Unstoppable Domains if no addresses are found in each of the Twitter user's bio, location, and pinned tweet. + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + final formattedName = query.replaceFirst("@", ""); + try { + final result = {}; + + final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName); + + final bio = (text: twitterUser.description, whitespaces: true); + final location = (text: twitterUser.location, whitespaces: false); + final pinnedTweet = (text: twitterUser.pinnedTweet?.text ?? '', whitespaces: true); + + final twitterTexts = [ + bio, + location, + pinnedTweet, + ].where((item) => item.text.isNotEmpty); + + for (final item in twitterTexts) { + result.addAll( + AddressResolverUtils.extractAddressesFromText( + raw: item.text, + currencies: currencies, + requireSurroundingWhitespaces: item.whitespaces, + ), + ); + + if (result.isEmpty) { + final domainResult = await AddressResolverUtils.resolveUnstoppableDomainFromText( + raw: item.text, + currencies: currencies, + ); + + if (domainResult.isNotEmpty) { + return [_buildTwitterParsedAddress(query, twitterUser, domainResult)]; + } + } + } + + if (result.isEmpty) return []; + + return [_buildTwitterParsedAddress(query, twitterUser, result)]; + } catch (e) { + printV('[address resolver service] Twitter error: $e'); + return []; + } + } + + ParsedAddress _buildTwitterParsedAddress( + String query, + TwitterUser twitterUser, + Map result, + ) { + return ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.twitter, + handle: query, + profileImageUrl: twitterUser.profileImageUrl, + profileName: twitterUser.name, + ); + } +} diff --git a/lib/twitter/twitter_api.dart b/lib/core/address_resolver/twitter/twitter_api.dart similarity index 96% rename from lib/twitter/twitter_api.dart rename to lib/core/address_resolver/twitter/twitter_api.dart index a508b3edf..0079d39a9 100644 --- a/lib/twitter/twitter_api.dart +++ b/lib/core/address_resolver/twitter/twitter_api.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/twitter/twitter_user.dart'; +import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; class TwitterApi { diff --git a/lib/twitter/twitter_user.dart b/lib/core/address_resolver/twitter/twitter_user.dart similarity index 100% rename from lib/twitter/twitter_user.dart rename to lib/core/address_resolver/twitter/twitter_user.dart diff --git a/lib/core/address_resolver/unstoppable/unstoppable_address_provider.dart b/lib/core/address_resolver/unstoppable/unstoppable_address_provider.dart new file mode 100644 index 000000000..dd240f7ed --- /dev/null +++ b/lib/core/address_resolver/unstoppable/unstoppable_address_provider.dart @@ -0,0 +1,235 @@ +import 'dart:convert'; + +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/openalias/openalias_record.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/wallet_base.dart'; + +class UnstoppableAddressProvider extends AddressLookupProvider { + static const unstoppableDomains = [ + "888", + "academy", + "agency", + "altimist", + "anime", + "austin", + "bald", + "bay", + "benji", + "bet", + "binanceus", + "bitcoin", + "bitget", + "bitscrunch", + "blockchain", + "boomer", + "boston", + "ca", + "caw", + "cc", + "chat", + "chomp", + "clay", + "club", + "co", + "com", + "company", + "crypto", + "dao", + "design", + "dfz", + "digital", + "doga", + "donut", + "dream", + "email", + "emir", + "eth", + "ethermail", + "family", + "farms", + "finance", + "fun", + "fyi", + "games", + "global", + "go", + "group", + "guru", + "hi", + "hockey", + "host", + "info", + "io", + "klever", + "kresus", + "kryptic", + "lfg", + "life", + "live", + "llc", + "ltc", + "ltd", + "manga", + "me", + "media", + "metropolis", + "miami", + "miku", + "money", + "moon", + "mumu", + "net", + "network", + "news", + "nft", + "npc", + "onchain", + "online", + "org", + "podcast", + "pog", + "polygon", + "base", + "arbitrum", + "press", + "privacy", + "pro", + "propykeys", + "pudgy", + "pw", + "quantum", + "rad", + "raiin", + "retardio", + "rip", + "rocks", + "secret", + "services", + "site", + "smobler", + "social", + "solutions", + "space", + "stepn", + "store", + "studio", + "systems", + "tball", + "tea", + "team", + "tech", + "technology", + "today", + "tribe", + "u", + "ubu", + "uno", + "unstoppable", + "vip", + "wallet", + "website", + "wif", + "wifi", + "witg", + "work", + "world", + "wrkx", + "wtf", + "x", + "xmr", + "xyz", + "zil", + "zone", + "pizza" + ]; + + @override + AddressSource get source => AddressSource.unstoppableDomains; + + @override + List get supportedCurrencies => [CryptoCurrency.xmr, CryptoCurrency.btc]; + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsUnstoppableDomains; + + @override + bool canHandle(String query) { + // Unstoppable Domains handle example: name.crypto + final formattedName = OpenaliasRecord.formatDomainName(query); + final domainParts = formattedName.split('.'); + final name = domainParts.last; + return domainParts.length > 1 && + domainParts.first.isNotEmpty && + name.isNotEmpty && + unstoppableDomains.any((domain) => name.trim() == domain); + } + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final formattedName = OpenaliasRecord.formatDomainName(query); + final result = {}; + + for (final currency in currencies) { + final address = await fetchUnstoppableDomainAddress( + formattedName, + currency.title, + ); + + if (address.isNotEmpty) { + result[currency] = address; + } + } + + if (result.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, + profileName: formattedName, + addressSource: AddressSource.unstoppableDomains, + handle: formattedName, + ), + ]; + } catch (e) { + printV('[address resolver] Error resolving Unstoppable Domain: $e'); + return []; + } + } + + static Future fetchUnstoppableDomainAddress(String domain, String ticker) async { + var address = ''; + + try { + final uri = Uri.parse("https://api.unstoppabledomains.com/profile/public/${Uri.encodeQueryComponent(domain)}?fields=records"); + final response = await ProxyWrapper().get(clearnetUri: uri); + + final jsonParsed = json.decode(response.body) as Map; + if (jsonParsed["records"] == null) { + throw Exception(".records response from $uri is empty"); + }; + final records = jsonParsed["records"] as Map; + final key = "crypto.${ticker.toUpperCase()}.address"; + if (records[key] == null) { + throw Exception(".records.${key} response from $uri is empty"); + } + + return records[key] as String? ?? ''; + } catch (e) { + printV('Unstoppable domain error: ${e.toString()}'); + address = ''; + } + + return address; + } +} diff --git a/lib/core/address_resolver/wellknown/wellknown_address_provider.dart b/lib/core/address_resolver/wellknown/wellknown_address_provider.dart new file mode 100644 index 000000000..464ffd798 --- /dev/null +++ b/lib/core/address_resolver/wellknown/wellknown_address_provider.dart @@ -0,0 +1,49 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_resolver/wellknown/wellknown_record.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class WellKnownAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.wellKnown; + + @override + List get supportedCurrencies => [CryptoCurrency.nano]; + + @override + bool canHandle(String q) => q.contains('.') && q.contains('@'); + + // .well-known handle example: + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown; + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final rec = await WellKnownRecord.fetch(query, CryptoCurrency.nano); + if (rec == null || rec.address.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: {CryptoCurrency.nano: rec.address}, + addressSource: AddressSource.wellKnown, + handle: query, + profileName: rec.title ?? '', + profileImageUrl: rec.imageUrl ?? '', + ) + ]; + } catch (e) { + printV('[address resolver] Error resolving Well-Known address: $e'); + return []; + } + } +} diff --git a/lib/core/address_resolver/wellknown/wellknown_record.dart b/lib/core/address_resolver/wellknown/wellknown_record.dart new file mode 100644 index 000000000..b2ed2eb9c --- /dev/null +++ b/lib/core/address_resolver/wellknown/wellknown_record.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; + +class WellKnownRecord { + WellKnownRecord({ + required this.address, + required this.alias, + required this.imageUrl, + required this.title, + }); + + final String address; + final String alias; + final String? imageUrl; + final String? title; + + static const _jsonLocation = {CryptoCurrency.nano: 'nano-currency'}; + + static Future fetch( + String username, + CryptoCurrency currency, + ) async { + final location = _jsonLocation[currency]; + if (location == null) { + printV('well-known: unsupported coin $currency'); + return null; + } + + final parts = username.split('@'); + if (parts.length < 2) { + printV('well-known: missing @ in "$username"'); + return null; + } + final alias = (parts.length == 3 ? parts[1] : parts[0]).trim(); + final domain = parts.last.trim(); + final encoded = Uri.encodeComponent(alias.isEmpty ? '_' : alias); + + final uri = Uri.https( + domain, + '/.well-known/$location.json', + {'names': encoded}, + ); + + try { + final res = await ProxyWrapper().get( + clearnetUri: uri, + headers: {'Accept': 'application/json'}, + ); + + final cType = res.headers['content-type']?.toLowerCase() ?? ''; + if (res.statusCode != 200 || !cType.contains('application/json')) { + printV('well-known: $uri → ${res.statusCode} $cType'); + return null; + } + + final body = json.decode(res.body) as Map; + final items = body['names'] as List? ?? const []; + + for (final raw in items.whereType()) { + if (raw['name'].toString().toLowerCase() == alias.toLowerCase()) { + return WellKnownRecord( + address: raw['address']?.toString() ?? '', + alias: alias, + imageUrl: raw['image']?.toString(), + title: raw['name']?.toString(), + ); + } + } + printV('well-known: alias "$alias" not found in list'); + } catch (e) { + printV('well-known: network / JSON error → $e'); + } + return null; + } +} \ No newline at end of file diff --git a/lib/core/address_resolver/yat/yat_address_provider.dart b/lib/core/address_resolver/yat/yat_address_provider.dart new file mode 100644 index 000000000..d8ee3454b --- /dev/null +++ b/lib/core/address_resolver/yat/yat_address_provider.dart @@ -0,0 +1,59 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart'; +import 'package:cake_wallet/entities/emoji_string_extension.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class YatAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.yatRecord; + + @override + List get supportedCurrencies => + [CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth, CryptoCurrency.ltc]; + + @override + bool canHandle(String q) => q.hasOnlyEmojis; // Yat handle example: 🐶🐾 + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown; + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final result = {}; + + for (final cur in currencies) { + final records = await YatService.fetchYatAddress(query, cur.title); + if (records.isEmpty) continue; + + final chosen = cur == CryptoCurrency.xmr + ? records.firstWhere((r) => r.isMoneroSub, orElse: () => records.first) + : records.first; + + result[cur] = chosen.address; + } + + if (result.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: result, + addressSource: AddressSource.yatRecord, + handle: query, + ) + ]; + } catch (e) { + printV('[address resolver] Error resolving Yat address: $e'); + return []; + } + } +} diff --git a/lib/core/address_resolver/yat/yat_record.dart b/lib/core/address_resolver/yat/yat_record.dart new file mode 100644 index 000000000..412a5a486 --- /dev/null +++ b/lib/core/address_resolver/yat/yat_record.dart @@ -0,0 +1,19 @@ +import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart'; + +class YatRecord { + YatRecord({ + required this.address, + required this.tag, + }); + + final String address; + final String tag; + + factory YatRecord.fromJson(Map json, String tag) => + YatRecord(address: (json['address'] ?? '').toString(), tag: tag); + + bool get isMoneroSub => tag == YatService.MONERO_SUB_ADDRESS; + + @override + String toString() => 'YatRecord(tag: $tag, address: $address)'; +} \ No newline at end of file diff --git a/lib/core/address_resolver/yat/yat_service.dart b/lib/core/address_resolver/yat/yat_service.dart new file mode 100644 index 000000000..84ba4e8f3 --- /dev/null +++ b/lib/core/address_resolver/yat/yat_service.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +import 'package:cake_wallet/core/address_resolver/yat/yat_record.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; + +class YatService { + static bool isDevMode = false; + + static String get apiUrl => + YatService.isDevMode ? YatService.apiDevUrl : YatService.apiReleaseUrl; + static const apiReleaseUrl = "https://a.y.at"; + static const apiDevUrl = 'https://a.yat.fyi'; + + static String lookupEmojiUrl(String emojiId) => + "$apiUrl/emoji_id/$emojiId/payment"; + + static const String MONERO_SUB_ADDRESS = '0x1002'; + static const String MONERO_STD_ADDRESS = '0x1001'; + static const tags = { + 'XMR': "$MONERO_STD_ADDRESS,$MONERO_SUB_ADDRESS", + 'BTC': '0x1003', + 'LTC': '0x1019' + }; + + static Future> fetchYatAddress( + String emojiId, + String ticker, + ) async { + final tagQuery = tags[ticker.toUpperCase()]; + if (tagQuery == null) return const []; + + final uri = Uri.parse( + lookupEmojiUrl(emojiId.replaceAll(' ', '')), + ).replace(queryParameters: {'tags': tagQuery}); + + try { + final response = await ProxyWrapper().get(clearnetUri: uri); + final body = json.decode(response.body) as Map; + + final res = body['result']; + if (res == null) return const []; + + final records = []; + + if (res is Map) { + res.forEach((tag, data) { + if (data is Map) { + records.add(YatRecord.fromJson(data, tag.toString())); + } + }); + } + + else if (res is List) { + for (final item in res) { + if (item is Map) { + final tag = item['tag']?.toString() ?? ''; + records.add(YatRecord.fromJson(item, tag)); + } + } + } + + return records; + } catch (_) { + return const []; + } + } +} \ No newline at end of file diff --git a/lib/store/yat/yat_store.dart b/lib/core/address_resolver/yat/yat_store.dart similarity index 100% rename from lib/store/yat/yat_store.dart rename to lib/core/address_resolver/yat/yat_store.dart diff --git a/lib/core/address_resolver/zano/zano_alias_address_provider.dart b/lib/core/address_resolver/zano/zano_alias_address_provider.dart new file mode 100644 index 000000000..e02856405 --- /dev/null +++ b/lib/core/address_resolver/zano/zano_alias_address_provider.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/wallet_base.dart'; + +class ZanoAliasAddressProvider extends AddressLookupProvider { + + @override + AddressSource get source => AddressSource.zanoAlias; + + @override + List get supportedCurrencies => [CryptoCurrency.zano]; + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZanoAlias; + + @override + bool canHandle(String query) => query.startsWith('@'); // zano handle example: @username + + static const _zanoUrl = 'http://37.27.100.59:10500/json_rpc'; + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + if (!currencies.contains(CryptoCurrency.zano)) return []; + + final formattedName = query.replaceFirst("@", ""); + if (formattedName.isEmpty) return []; + + final zanoAddress = await fetchZanoAliasAddress(formattedName); + if (zanoAddress == null || zanoAddress.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: {CryptoCurrency.zano: zanoAddress}, + addressSource: AddressSource.zanoAlias, + handle: query, + ), + ]; + } catch (e) { + printV('[address resolver] Error resolving Zano Alias: $e'); + return []; + } + } + + static Future fetchZanoAliasAddress(String alias) async { + try { + final uri = Uri.parse(_zanoUrl); + final response = await ProxyWrapper().post( + clearnetUri: uri, + body: json.encode({ + 'id': 0, + 'jsonrpc': '2.0', + 'method': 'get_alias_details', + 'params': {'alias': alias}, + }), + ); + + final jsonParsed = json.decode(response.body) as Map; + return jsonParsed['result']?['alias_details']?['address'] as String?; + } catch (e) { + printV('[address resolver] Zano Alias error: $e'); + return null; + } + } +} diff --git a/lib/core/address_resolver/zcash/zcash_address_provider.dart b/lib/core/address_resolver/zcash/zcash_address_provider.dart new file mode 100644 index 000000000..5da25fdd4 --- /dev/null +++ b/lib/core/address_resolver/zcash/zcash_address_provider.dart @@ -0,0 +1,49 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_resolver/zcash/zcash_names_record.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; + +class ZcashNameAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.zcashName; + + @override + List get supportedCurrencies => [CryptoCurrency.zec]; + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames; + + @override + bool canHandle(String query) { + final lowerText = query.toLowerCase(); + return lowerText.endsWith(".zec") || lowerText.endsWith(".zcash"); + } // Zcash handle example: name.zec + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final address = await ZcashNamesRecord.fetchZcashNamesAddress(query); + if (address == null || address.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: {CryptoCurrency.zec: address}, + profileName: query, + addressSource: AddressSource.zcashName, + handle: query, + ), + ]; + } catch (e) { + printV('[address resolver] Error resolving Zcash Name: $e'); + return []; + } + } +} diff --git a/lib/core/address_resolver/zcash/zcash_me_address_provider.dart b/lib/core/address_resolver/zcash/zcash_me_address_provider.dart new file mode 100644 index 000000000..6dd0968d9 --- /dev/null +++ b/lib/core/address_resolver/zcash/zcash_me_address_provider.dart @@ -0,0 +1,74 @@ +import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/wallet_base.dart'; + +class ZcashMeAddressProvider extends AddressLookupProvider { + @override + AddressSource get source => AddressSource.zcashAddress; + + @override + List get supportedCurrencies => [CryptoCurrency.zec]; + + @override + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames; + + @override + bool canHandle(String query) { + final value = query.trim().toLowerCase(); + return value.startsWith('zcash.me/') && value.split('/').last.isNotEmpty; + } // zcash.me profile example: zcash.me/username + + @override + Future> resolve({ + required String query, + required List currencies, + required WalletBase wallet, + }) async { + try { + final handle = query.trim().split('/').last; + if (handle.isEmpty) return []; + + final address = await _fetchZcashAddress(handle); + if (address == null || address.isEmpty) return []; + + return [ + ParsedAddress( + parsedAddressByCurrencyMap: {CryptoCurrency.zec: address}, + profileName: handle, + addressSource: AddressSource.zcashAddress, + handle: query, + ), + ]; + } catch (e) { + printV('[address resolver] Error resolving zcash.me address: $e'); + return []; + } + } + + Future _fetchZcashAddress(String handle) async { + final url = Uri.parse('https://zcash.me/$handle'); + + try { + final response = await ProxyWrapper().get(clearnetUri: url); + + if (response.statusCode == 200) { + final addressRegex = RegExp( + r'(t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300})', + caseSensitive: true, + ); + + final match = addressRegex.firstMatch(response.body); + return match?.group(0); + } + } catch (e) { + printV('[address resolver] Error fetching zcash.me profile: $e'); + } + + return null; + } +} diff --git a/lib/entities/zcash_names_record.dart b/lib/core/address_resolver/zcash/zcash_names_record.dart similarity index 100% rename from lib/entities/zcash_names_record.dart rename to lib/core/address_resolver/zcash/zcash_names_record.dart diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 0d44bad67..d6934542e 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -12,29 +12,54 @@ const AFTER_REGEX = '(\$|\\s)'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type, bool isTestnet = false}) : super( - errorMessage: S.current.error_text_address, - useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type) - ? (String txt) { - final RegExp lightningInvoiceRegex = RegExp( - r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$', - caseSensitive: false); - if (lightningInvoiceRegex.hasMatch(txt)) return true; + errorMessage: S.current.error_text_address, + useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type) + ? (String txt) { + final RegExp lightningInvoiceRegex = RegExp( + r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$', + caseSensitive: false); + if (lightningInvoiceRegex.hasMatch(txt)) return true; - return BitcoinAddressUtils.validateAddress( - address: txt, - network: type == CryptoCurrency.btc - ? isTestnet - ? BitcoinNetwork.testnet - : BitcoinNetwork.mainnet - : LitecoinNetwork.mainnet, - ); - } - : type == CryptoCurrency.zano - ? zano?.validateAddress - : null, - pattern: getPattern(type, isTestnet: isTestnet), - length: getLength(type), - ); + return BitcoinAddressUtils.validateAddress( + address: txt, + network: type == CryptoCurrency.btc + ? isTestnet + ? BitcoinNetwork.testnet + : BitcoinNetwork.mainnet + : LitecoinNetwork.mainnet, + ); + } + : type == CryptoCurrency.zano + ? zano?.validateAddress + : null, + pattern: getPattern(type, isTestnet: isTestnet), + length: getLength(type), + ); + + static const List reliableValidateCurrencies = [ + CryptoCurrency.xmr, + CryptoCurrency.btc, + CryptoCurrency.ltc, + CryptoCurrency.bch, + CryptoCurrency.trx, + CryptoCurrency.nano, + CryptoCurrency.sol, + CryptoCurrency.zano, + CryptoCurrency.ada, + CryptoCurrency.xrp, + CryptoCurrency.xhv, + CryptoCurrency.zaddr, + CryptoCurrency.zec, + CryptoCurrency.dcr, + CryptoCurrency.rvn, + CryptoCurrency.near, + CryptoCurrency.rune, + CryptoCurrency.scrt, + CryptoCurrency.stx, + CryptoCurrency.kmd, + CryptoCurrency.doge, + CryptoCurrency.btcln, + ]; static String getPattern(CryptoCurrency type, {bool isTestnet = false}) { var pattern = ""; @@ -146,13 +171,13 @@ class AddressValidator extends TextValidator { pattern = '[0-9a-zA-Z.]+'; case CryptoCurrency.zec: pattern = - '(?:' - 't1[0-9A-Za-z]{33}' - '|t3[0-9A-Za-z]{33}' - '|zs[a-z0-9]{76}' - '|zxviews[a-z0-9]{278}' - '|u1[a-z0-9]{1,300}' - ')'; + '(?:' + 't1[0-9A-Za-z]{33}' + '|t3[0-9A-Za-z]{33}' + '|zs[a-z0-9]{76}' + '|zxviews[a-z0-9]{278}' + '|u1[a-z0-9]{1,300}' + ')'; case CryptoCurrency.dcr: pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: @@ -344,7 +369,7 @@ class AddressValidator extends TextValidator { '|([0-9a-zA-Z]{106})'; case CryptoCurrency.btc: pattern = - '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: pattern = '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' @@ -394,4 +419,4 @@ class AddressValidator extends TextValidator { return pattern != null ? "($pattern)" : null; } -} +} \ No newline at end of file diff --git a/lib/core/yat_service.dart b/lib/core/yat_service.dart deleted file mode 100644 index 84aac1626..000000000 --- a/lib/core/yat_service.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:convert'; - -import 'package:cake_wallet/entities/yat_record.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; - -class YatService { - static bool isDevMode = false; - - static String get apiUrl => - YatService.isDevMode ? YatService.apiDevUrl : YatService.apiReleaseUrl; - static const apiReleaseUrl = "https://a.y.at"; - static const apiDevUrl = 'https://a.yat.fyi'; - - static String lookupEmojiUrl(String emojiId) => - "$apiUrl/emoji_id/$emojiId/payment"; - - static const String MONERO_SUB_ADDRESS = '0x1002'; - static const String MONERO_STD_ADDRESS = '0x1001'; - static const tags = { - 'XMR': "$MONERO_STD_ADDRESS,$MONERO_SUB_ADDRESS", - 'BTC': '0x1003', - 'LTC': '0x1019' - }; - - Future> fetchYatAddress(String emojiId, String ticker) async { - final formattedTicker = ticker.toUpperCase(); - final formattedEmojiId = emojiId.replaceAll(' ', ''); - final tag = tags[formattedTicker]; - final uri = Uri.parse(lookupEmojiUrl(formattedEmojiId)).replace( - queryParameters: { - "tags": tag - }); - final yatRecords = []; - - try { - final response = await ProxyWrapper().get(clearnetUri: uri); - - final resBody = json.decode(response.body) as Map; - final results = resBody["result"] as Map; - // Favour a subaddress over a standard address. - final yatRecord = ( - results[MONERO_SUB_ADDRESS] ?? - results[MONERO_STD_ADDRESS] ?? - results[tag]) as Map; - - if (yatRecord.isNotEmpty) { - yatRecords.add(YatRecord.fromJson(yatRecord)); - } - - return yatRecords; - } catch (_) { - return yatRecords; - } - } -} diff --git a/lib/di.dart b/lib/di.dart index 821f51130..bf2b64443 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -6,6 +6,9 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/anypay/anypay_api.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'; +import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart'; +import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart'; import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; @@ -29,7 +32,6 @@ import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/trade_monitor.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; -import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/bridge_transfer.dart'; @@ -37,7 +39,6 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart'; -import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/template.dart'; @@ -198,7 +199,6 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/store/templates/send_template_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; -import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/themes/core/theme_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/device_info.dart'; @@ -899,6 +899,7 @@ Future setup({ getIt.get(), getIt.get(), getIt.get(), + getIt.get(), getIt.get(), getIt.get(), _transactionDescriptionBox, @@ -1100,7 +1101,10 @@ Future setup({ AnimatedURPage(getIt.get(), urQr: urQr)); getIt.registerFactoryParam( - (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact)); + (ContactRecord? contact, _) => ContactViewModel(_contactSource, + getIt.get(), + getIt.get(), + contact: contact)); getIt.registerFactoryParam( (CryptoCurrency? cur, _) => @@ -1249,7 +1253,10 @@ Future setup({ getIt.registerFactoryParam( (PaymentRequest? paymentRequest, __) { - return ExchangePage(getIt.get(), getIt.get(), paymentRequest); + return ExchangePage(getIt.get(), + getIt.get(), + getIt.get(), + paymentRequest); }); getIt.registerFactoryParam( @@ -1257,6 +1264,7 @@ Future setup({ return NewSwapPage( getIt.get(), getIt.get(), + getIt.get(), paymentRequest, walletSwitcherViewModel: getIt.get(), initialCurrency: initialCurrency, @@ -1494,7 +1502,7 @@ Future setup({ getIt.registerFactory(() => BuySellViewModel(getIt.get())); - getIt.registerFactory(() => BuySellPage(getIt.get())); + getIt.registerFactory(() => BuySellPage(getIt.get(), getIt.get())); getIt.registerFactoryParam, void>((List args, _) { final items = args.first as List; @@ -1592,10 +1600,9 @@ Future setup({ getIt.registerFactory(() => YatService()); - getIt.registerFactory(() => AddressResolver( - yatService: getIt.get(), - wallet: getIt.get().wallet!, - settingsStore: getIt.get())); + getIt.registerFactory( + () => AddressResolverService(settingsStore: getIt.get()), + ); getIt.registerFactoryParam( (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData)); diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart deleted file mode 100644 index 239b5f45c..000000000 --- a/lib/entities/ens_record.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:ens_dart/ens_dart.dart'; -import 'package:web3dart/web3dart.dart'; - -class EnsRecord { - static Future fetchEnsAddress(String name, {WalletBase? wallet}) async { - Web3Client? _client; - - if (wallet != null && (isEVMCompatibleChain(wallet.type))) { - _client = evm!.getWeb3Client(wallet); - } - - if (_client == null) { - late final client = ProxyWrapper().getHttpIOClient(); - - _client = Web3Client("https://ethereum-rpc.publicnode.com", client); - } - - try { - final ens = Ens(client: _client); - - if (wallet != null) { - switch (wallet.type) { - case WalletType.monero: - return await ens.withName(name).getCoinAddress(CoinType.XMR); - case WalletType.bitcoin: - return await ens.withName(name).getCoinAddress(CoinType.BTC); - case WalletType.litecoin: - return await ens.withName(name).getCoinAddress(CoinType.LTC); - case WalletType.haven: - return await ens.withName(name).getCoinAddress(CoinType.XHV); - case WalletType.ethereum: - case WalletType.polygon: - case WalletType.base: - case WalletType.arbitrum: - case WalletType.bsc: - default: - return (await ens.withName(name).getAddress()).hex; - } - } - - final addr = await ens.withName(name).getAddress(); - return addr.hex; - } catch (e) { - printV(e); - return ""; - } - } -} diff --git a/lib/entities/fio_address_provider.dart b/lib/entities/fio_address_provider.dart deleted file mode 100644 index dcfd79c96..000000000 --- a/lib/entities/fio_address_provider.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:convert'; - -import 'package:cw_core/utils/proxy_wrapper.dart'; - -class FioAddressProvider { - static const apiAuthority = 'fio.blockpane.com'; - static const availCheck = '/v1/chain/avail_check'; - static const getAddress = '/v1/chain/get_pub_address'; - - static Future checkAvail(String fioAddress) async { - bool isFioRegistered = false; - final headers = {'Content-Type': 'application/json'}; - final body = {"fio_name": fioAddress}; - - final uri = Uri.https(apiAuthority, availCheck); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(body), - ); - - if (response.statusCode != 200) { - return isFioRegistered; - } - - - final responseJSON = json.decode(response.body) as Map; - isFioRegistered = responseJSON['is_registered'] as int == 1; - - return isFioRegistered; - } - - static Future getPubAddress(String fioAddress, String token) async { - final headers = {'Content-Type': 'application/json'}; - final body = { - "fio_address": fioAddress, - "chain_code": token.toUpperCase(), - "token_code": token.toUpperCase(), - }; - - final uri = Uri.https(apiAuthority, getAddress); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(body), - ); - - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error'] as String; - final message = responseJSON['message'] as String; - throw Exception('${error}\n$message'); - } - - if (response.statusCode != 200) { - throw Exception('Unexpected response http status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body) as Map; - final String pubAddress = responseJSON['public_address'] as String; - - return pubAddress; - } -} diff --git a/lib/entities/openalias_record.dart b/lib/entities/openalias_record.dart deleted file mode 100644 index b2af2e53d..000000000 --- a/lib/entities/openalias_record.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:basic_utils/basic_utils.dart'; -import 'package:cw_core/utils/print_verbose.dart'; - -class OpenaliasRecord { - OpenaliasRecord({ - required this.address, - required this.name, - required this.description, - }); - - final String name; - final String address; - final String description; - - static String formatDomainName(String name) { - String formattedName = name; - - if (name.contains("@")) { - formattedName = name.replaceAll("@", "."); - } - - return formattedName; - } - - static Future?> lookupOpenAliasRecord(String name) async { - try { - final txtRecord = await DnsUtils.lookupRecord(name, RRecordType.TXT, dnssec: true); - - return txtRecord; - } catch (e) { - printV("${e.toString()}"); - return null; - } - } - - static OpenaliasRecord fetchAddressAndName({ - required String formattedName, - required String ticker, - required List txtRecord, - }) { - String address = ''; - String name = formattedName; - String note = ''; - - for (RRecord element in txtRecord) { - String record = element.data; - - if (record.contains("oa1:$ticker") && record.contains("recipient_address")) { - record = record.replaceAll('\"', ""); - - final dataList = record.split(";"); - - address = dataList - .where((item) => (item.contains("recipient_address"))) - .toString() - .replaceAll("oa1:$ticker recipient_address=", "") - .replaceAll("(", "") - .replaceAll(")", "") - .trim(); - - final recipientName = dataList - .where((item) => (item.contains("recipient_name"))) - .toString() - .replaceAll("(", "") - .replaceAll(")", "") - .trim(); - - if (recipientName.isNotEmpty) { - name = recipientName.replaceAll("recipient_name=", ""); - } - - final description = dataList - .where((item) => (item.contains("tx_description"))) - .toString() - .replaceAll("(", "") - .replaceAll(")", "") - .trim(); - - if (description.isNotEmpty) { - note = description.replaceAll("tx_description=", ""); - } - - break; - } - - } - return OpenaliasRecord(address: address, name: name, description: note); -} -} diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart deleted file mode 100644 index 7a6a47bc8..000000000 --- a/lib/entities/parse_address_from_domain.dart +++ /dev/null @@ -1,549 +0,0 @@ -import 'package:cake_wallet/core/address_validator.dart'; -import 'package:cake_wallet/core/yat_service.dart'; -import 'package:cake_wallet/entities/emoji_string_extension.dart'; -import 'package:cake_wallet/entities/ens_record.dart'; -import 'package:cake_wallet/entities/lnurlpay_record.dart'; -import 'package:cake_wallet/entities/fio_address_provider.dart'; -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/wellknown_record.dart'; -import 'package:cake_wallet/entities/zano_alias.dart'; -import 'package:cake_wallet/entities/zcash_names_record.dart'; -import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; -import 'package:cake_wallet/mastodon/mastodon_api.dart'; -import 'package:cake_wallet/nostr/nostr_api.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/twitter/twitter_api.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/cupertino.dart'; - -import 'bip_353_record.dart'; - -class AddressResolver { - AddressResolver({required this.yatService, required this.wallet, required this.settingsStore}) - : walletType = wallet.type; - - final YatService yatService; - final WalletType walletType; - final WalletBase wallet; - final SettingsStore settingsStore; - - static const unstoppableDomains = [ - "888", - "academy", - "agency", - "altimist", - "anime", - "austin", - "bald", - "bay", - "benji", - "bet", - "binanceus", - "bitcoin", - "bitget", - "bitscrunch", - "blockchain", - "boomer", - "boston", - "ca", - "caw", - "cc", - "chat", - "chomp", - "clay", - "club", - "co", - "com", - "company", - "crypto", - "dao", - "design", - "dfz", - "digital", - "doga", - "donut", - "dream", - "email", - "emir", - "eth", - "ethermail", - "family", - "farms", - "finance", - "fun", - "fyi", - "games", - "global", - "go", - "group", - "guru", - "hi", - "hockey", - "host", - "info", - "io", - "klever", - "kresus", - "kryptic", - "lfg", - "life", - "live", - "llc", - "ltc", - "ltd", - "manga", - "me", - "media", - "metropolis", - "miami", - "miku", - "money", - "moon", - "mumu", - "net", - "network", - "news", - "nft", - "npc", - "onchain", - "online", - "org", - "podcast", - "pog", - "polygon", - "base", - "arbitrum" - "press", - "privacy", - "pro", - "propykeys", - "pudgy", - "pw", - "quantum", - "rad", - "raiin", - "retardio", - "rip", - "rocks", - "secret", - "services", - "site", - "smobler", - "social", - "solutions", - "space", - "stepn", - "store", - "studio", - "systems", - "tball", - "tea", - "team", - "tech", - "technology", - "today", - "tribe", - "u", - "ubu", - "uno", - "unstoppable", - "vip", - "wallet", - "website", - "wif", - "wifi", - "witg", - "work", - "world", - "wrkx", - "wtf", - "x", - "xmr", - "xyz", - "zil", - "zone", - "pizza" - ]; - - static String? extractAddressByType( - {required String raw, - required CryptoCurrency type, - bool requireSurroundingWhitespaces = true}) { - var addressPattern = AddressValidator.getAddressFromStringPattern(type); - - if (addressPattern == null) { - return null; - } - - if (requireSurroundingWhitespaces) - addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX"; - - final match = RegExp(addressPattern, multiLine: true).firstMatch(raw); - return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), - (Match match) { - String group = match.group(0)!; - if (group.startsWith('bitcoincash:') || - group.startsWith('nano_') || - group.startsWith('ban_')) { - return group; - } - return ''; - }); - } - - static String extractUnstoppableDomain(String raw) { - // sort by length to avoid matching shorter tld instead of longer - final domains = List.from(unstoppableDomains)..sort((a, b) => b.length.compareTo(a.length)); - for (final tld in domains) { - final pattern = RegExp(r'([A-Za-z0-9-]+)\.' + RegExp.escape(tld), caseSensitive: false); - final match = pattern.firstMatch(raw); - if (match != null) return match.group(0)!; - } - return ''; - } - - bool isEmailFormat(String address) { - final RegExp emailRegex = RegExp( - r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', - caseSensitive: false, - ); - return emailRegex.hasMatch(address); - } - - String _stripHtmlTags(String value) { - return value - .replaceAll(RegExp(r'', caseSensitive: false), '\n') - .replaceAll(RegExp(r'', caseSensitive: false), '\n') - .replaceAll(RegExp(r'<[^>]*>'), '') - .trim(); - } - - Future resolve(BuildContext context, String text, CryptoCurrency currency) async { - final ticker = currency.title; - try { - if (text.startsWith("zcash.me")) { - final parts = text.split("/"); - final handle = parts.last; - if (parts.length == 2 && handle.isNotEmpty) { - final extractZcashAddress = await _fetchZcashAddress(handle); - if (extractZcashAddress != null) { - return ParsedAddress.zcashAddress( - address: extractZcashAddress, - name: handle, - ); - } - } - } - // twitter handle example: @username - if (text.startsWith('@') && !text.substring(1).contains('@')) { - if (currency == CryptoCurrency.zano && settingsStore.lookupsZanoAlias) { - final formattedName = text.substring(1); - final zanoAddress = await ZanoAlias.fetchZanoAliasAddress(formattedName); - if (zanoAddress != null && zanoAddress.isNotEmpty) { - return ParsedAddress.zanoAddress( - address: zanoAddress, - name: text, - ); - } - } - if (settingsStore.lookupsTwitter) { - final formattedName = text.substring(1); - final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName); - final addressFromBio = extractAddressByType( - raw: twitterUser.description, - type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency)); - if (addressFromBio != null && addressFromBio.isNotEmpty) { - return ParsedAddress.fetchTwitterAddress( - address: addressFromBio, - name: text, - profileImageUrl: twitterUser.profileImageUrl, - profileName: twitterUser.name); - } else { - final domain = extractUnstoppableDomain(twitterUser.description); - if (domain.isNotEmpty) { - final parsedAddressFromDomain = await resolve(context, domain, currency); - if (parsedAddressFromDomain.addresses.isNotEmpty && - parsedAddressFromDomain.addresses.first != domain) { - return ParsedAddress( - addresses: parsedAddressFromDomain.addresses, - name: text, - profileImageUrl: twitterUser.profileImageUrl, - profileName: twitterUser.name, - parseFrom: ParseFrom.twitter, - ); - } - - } - - } - - final addressFromLocation = extractAddressByType( - raw: twitterUser.location, - type: CryptoCurrency.fromString(ticker, - walletCurrency: wallet.currency), requireSurroundingWhitespaces: false); - if (addressFromLocation != null && addressFromLocation.isNotEmpty) { - return ParsedAddress.fetchTwitterAddress( - address: addressFromLocation, - name: text, - profileImageUrl: twitterUser.profileImageUrl, - profileName: twitterUser.name); - } else { - final domain = extractUnstoppableDomain(twitterUser.location); - if (domain.isNotEmpty) { - final parsedAddressFromDomain = await resolve(context, domain, currency); - if (parsedAddressFromDomain.addresses.isNotEmpty && - parsedAddressFromDomain.addresses.first != domain) { - return ParsedAddress( - addresses: parsedAddressFromDomain.addresses, - name: text, - profileImageUrl: twitterUser.profileImageUrl, - profileName: twitterUser.name, - parseFrom: ParseFrom.twitter, - ); - } - - } - - } - - final pinnedTweet = twitterUser.pinnedTweet?.text; - if (pinnedTweet != null) { - final addressFromPinnedTweet = extractAddressByType( - raw: pinnedTweet, - type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency), - requireSurroundingWhitespaces: true); - if (addressFromPinnedTweet != null) { - return ParsedAddress.fetchTwitterAddress( - address: addressFromPinnedTweet, - name: text, - profileImageUrl: twitterUser.profileImageUrl, - profileName: twitterUser.name); - } else { - final domain = extractUnstoppableDomain(pinnedTweet); - if (domain.isNotEmpty) { - final parsedAddressFromDomain = await resolve(context, domain, currency); - if (parsedAddressFromDomain.addresses.isNotEmpty && - parsedAddressFromDomain.addresses.first != domain) { - return ParsedAddress( - addresses: parsedAddressFromDomain.addresses, - name: text, - profileImageUrl: twitterUser.profileImageUrl, - profileName: twitterUser.name, - parseFrom: ParseFrom.twitter, - ); - } - } - } - } - } - } - - // Mastodon example: @username@hostname.xxx - if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) { - if (settingsStore.lookupsMastodon) { - 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) { - final mastodonNote = _stripHtmlTags(mastodonUser.note); - String? addressFromBio = extractAddressByType(raw: mastodonNote, type: currency, requireSurroundingWhitespaces: true); - - if (addressFromBio != null && addressFromBio.isNotEmpty) { - return ParsedAddress.fetchMastodonAddress( - address: addressFromBio, - name: text, - profileImageUrl: mastodonUser.profileImageUrl, - profileName: mastodonUser.username); - } else { - final pinnedPosts = - await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName); - - if (pinnedPosts.isNotEmpty) { - final userPinnedPostsText = pinnedPosts.map((item) => _stripHtmlTags(item.content)).join('\n'); - String? addressFromPinnedPost = - extractAddressByType(raw: userPinnedPostsText, type: currency, - requireSurroundingWhitespaces: true); - - if (addressFromPinnedPost != null && addressFromPinnedPost.isNotEmpty) { - return ParsedAddress.fetchMastodonAddress( - address: addressFromPinnedPost, - name: text, - profileImageUrl: mastodonUser.profileImageUrl, - profileName: mastodonUser.username); - } - } - } - } - } - } - - // .well-known scheme: - if (text.contains('.') && text.contains('@')) { - if (settingsStore.lookupsWellKnown) { - final record = - await WellKnownRecord.fetchAddressAndName(formattedName: text, currency: currency); - if (record != null) { - return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text); - } - } - - if (walletType == WalletType.bitcoin) { - final record = - await LNUrlPayRecord.fetchAddressAndName(formattedName: text, currency: currency); - if (record != null) { - return ParsedAddress.fetchLNUrlPayAddress(address: record.address, name: text); - } - } - } - - if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) { - final bool isFioRegistered = await FioAddressProvider.checkAvail(text); - if (isFioRegistered) { - final address = await FioAddressProvider.getPubAddress(text, ticker); - return ParsedAddress.fetchFioAddress(address: address, name: text); - } - } - if (text.hasOnlyEmojis) { - if (settingsStore.lookupsYatService) { - if (walletType != WalletType.haven) { - final addresses = await yatService.fetchYatAddress(text, ticker); - return ParsedAddress.fetchEmojiAddress(addresses: addresses, name: text); - } - } - } - - final isNormalAddress = extractAddressByType(raw: text, type: currency)?.isNotEmpty ?? false; - - if (text.length <= 30 && !isNormalAddress) { - final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text); - if (thorChainAddress != null && thorChainAddress.isNotEmpty) { - String? address = - thorChainAddress[ticker] ?? (ticker == 'RUNE' ? thorChainAddress['THOR'] : null); - if (address != null) { - return ParsedAddress.thorChainAddress(address: address, name: text); - } - } - } - - final formattedName = OpenaliasRecord.formatDomainName(text); - final domainParts = formattedName.split('.'); - final name = domainParts.last; - - if (domainParts.length <= 1 || domainParts.first.isEmpty || name.isEmpty) { - return ParsedAddress(addresses: [text]); - } - - if (unstoppableDomains.any((domain) => name.trim() == domain)) { - if (settingsStore.lookupsUnstoppableDomains) { - final address = await fetchUnstoppableDomainAddress(text, ticker); - if (address.isNotEmpty) { - return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text); - } - } - } - - final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, ticker); - - if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) { - final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap); - if (chosenAddress != null) { - try { - final dnsProof = await Bip353Record.fetchDnsProof(text); - return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text, dnsProof: dnsProof); - } catch (e) { - printV('Bip353Record.fetchBip353AddressAddress error: $e'); - return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text); - } - } - } - - final lowerText = text.toLowerCase(); - if (lowerText.endsWith(".zec") || lowerText.endsWith(".zcash")) { - if (settingsStore.lookupsZcashNames) { - final address = await ZcashNamesRecord.fetchZcashNamesAddress(text); - if (address != null && address.isNotEmpty) { - return ParsedAddress.zcashNameAddress(address: address, name: text); - } - } - } - - if (text.endsWith(".eth")) { - if (settingsStore.lookupsENS) { - final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet); - if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") { - return ParsedAddress.fetchEnsAddress(name: text, address: address); - } - } - } - - if (formattedName.contains(".")) { - if (settingsStore.lookupsOpenAlias) { - final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName); - - if (txtRecord != null) { - final record = await OpenaliasRecord.fetchAddressAndName( - formattedName: formattedName, ticker: ticker.toLowerCase(), txtRecord: txtRecord); - return ParsedAddress.fetchOpenAliasAddress(record: record, name: text); - } - } - } - if (isEmailFormat(text)) { - final nostrProfile = await NostrProfileHandler.queryProfile(context, text); - if (nostrProfile?.relays != null) { - final nostrUserData = - await NostrProfileHandler.processRelays(context, nostrProfile!, text); - - if (nostrUserData != null) { - String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency); - if (addressFromBio != null && addressFromBio.isNotEmpty) { - return ParsedAddress.nostrAddress( - address: addressFromBio, - name: text, - profileImageUrl: nostrUserData.picture, - profileName: nostrUserData.name); - } - } - } - } - } catch (e) { - printV(e.toString()); - } - - return ParsedAddress(addresses: [text]); - } - - Future _fetchZcashAddress(String handle) async { - final url = Uri.parse('https://zcash.me/$handle'); - - try { - final response = await ProxyWrapper().get(clearnetUri: url); - - if (response.statusCode == 200) { - final addressRegex = RegExp( - r'(t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300})', - caseSensitive: true, - ); - - final match = addressRegex.firstMatch(response.body); - - if (match != null) { - return match.group(0); - } - } - } catch (e) { - printV('Error fetching zcash.me profile: $e'); - } - return null; - } -} diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart deleted file mode 100644 index 28bdf704f..000000000 --- a/lib/entities/parsed_address.dart +++ /dev/null @@ -1,212 +0,0 @@ -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, - mastodon, - nostr, - thorChain, - wellKnown, - zanoAlias, - zcashAddress, - zcashName, - bip353, - lnurlpay, -} - -class ParsedAddress { - ParsedAddress({ - required this.addresses, - this.name = '', - this.description = '', - this.profileImageUrl = '', - this.profileName = '', - this.parseFrom = ParseFrom.notParsed, - this.bip353DnsProof, - }); - - factory ParsedAddress.fetchEmojiAddress({ - List? addresses, - required String name, - }) { - if (addresses?.isEmpty ?? true) { - return ParsedAddress(addresses: [name], parseFrom: ParseFrom.yatRecord); - } - return ParsedAddress( - addresses: addresses!.map((e) => e.address).toList(), - name: name, - parseFrom: ParseFrom.yatRecord, - ); - } - - factory ParsedAddress.fetchUnstoppableDomainAddress({ - String? address, - required String name, - }) { - if (address?.isEmpty ?? true) { - return ParsedAddress(addresses: [name]); - } - return ParsedAddress( - addresses: [address!], - name: name, - parseFrom: ParseFrom.unstoppableDomains, - ); - } - - factory ParsedAddress.fetchBip353AddressAddress ({ - required String address, - required String name, - String? dnsProof, - }) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.bip353, - bip353DnsProof: dnsProof, - ); - } - - factory ParsedAddress.fetchOpenAliasAddress( - {required OpenaliasRecord record, required String name}) { - if (record.address.isEmpty) { - return ParsedAddress(addresses: [name]); - } - return ParsedAddress( - addresses: [record.address], - name: record.name, - description: record.description, - parseFrom: ParseFrom.openAlias, - ); - } - - factory ParsedAddress.fetchFioAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.fio, - ); - } - - factory ParsedAddress.fetchTwitterAddress( - {required String address, - required String name, - required String profileImageUrl, - required String profileName, - String? description}) { - return ParsedAddress( - addresses: [address], - name: name, - description: description ?? '', - profileImageUrl: profileImageUrl, - profileName: profileName, - parseFrom: ParseFrom.twitter, - ); - } - - factory ParsedAddress.fetchMastodonAddress( - {required String address, - required String name, - required String profileImageUrl, - required String profileName}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.mastodon, - profileImageUrl: profileImageUrl, - profileName: profileName, - ); - } - - factory ParsedAddress.fetchContactAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.contact, - ); - } - - factory ParsedAddress.fetchEnsAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.ens, - ); - } - - factory ParsedAddress.nostrAddress( - {required String address, - required String name, - required String profileImageUrl, - required String profileName}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.nostr, - profileImageUrl: profileImageUrl, - profileName: profileName, - ); - } - - factory ParsedAddress.thorChainAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.thorChain, - ); - } - - factory ParsedAddress.zanoAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.zanoAlias, - ); - } - - factory ParsedAddress.zcashAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.zcashAddress, - ); - } - - factory ParsedAddress.zcashNameAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.zcashName, - ); - } - - factory ParsedAddress.fetchWellKnownAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.wellKnown, - ); - } - - factory ParsedAddress.fetchLNUrlPayAddress({required String address, required String name}) { - return ParsedAddress( - addresses: [address], - name: name, - parseFrom: ParseFrom.lnurlpay, - ); - } - - final List addresses; - final String name; - final String description; - final String profileImageUrl; - final String profileName; - final ParseFrom parseFrom; - final String? bip353DnsProof; -} diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index be90ed6ab..c052633a0 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -102,7 +102,13 @@ class PreferencesKey { static const lookupsOpenAlias = 'looks_up_open_alias'; static const lookupsENS = 'looks_up_ens'; static const lookupsZcashNames = 'looks_up_zcash_names'; + static const lookupsZcashAddress = 'looks_up_zcash_address'; static const lookupsWellKnown = 'looks_up_well_known'; + static const lookupsFio = 'looks_up_fio'; + static const lookupsNostr = 'looks_up_nostr'; + static const lookupsThorChain = 'looks_up_thor_chain'; + static const lookupsBip353 = 'looks_up_bip353'; + static const lookupsLNUrl = 'looks_up_lnurl'; static const useBlinkProtection = 'use_blink_protection'; static const usePayjoin = 'use_payjoin'; static const showPayjoinCard = 'show_payjoin_card'; diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart deleted file mode 100644 index cf6eb8945..000000000 --- a/lib/entities/unstoppable_domain_address.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:convert'; - -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; - -Future fetchUnstoppableDomainAddress(String domain, String ticker) async { - var address = ''; - - try { - final uri = Uri.parse("https://api.unstoppabledomains.com/profile/public/${Uri.encodeQueryComponent(domain)}?fields=records"); - final response = await ProxyWrapper().get(clearnetUri: uri); - - final jsonParsed = json.decode(response.body) as Map; - if (jsonParsed["records"] == null) { - throw Exception(".records response from $uri is empty"); - }; - final records = jsonParsed["records"] as Map; - final key = "crypto.${ticker.toUpperCase()}.address"; - if (records[key] == null) { - throw Exception(".records.${key} response from $uri is empty"); - } - - return records[key] as String? ?? ''; - } catch (e) { - printV('Unstoppable domain error: ${e.toString()}'); - address = ''; - } - - return address; -} \ No newline at end of file diff --git a/lib/entities/wellknown_record.dart b/lib/entities/wellknown_record.dart deleted file mode 100644 index 6dd94440b..000000000 --- a/lib/entities/wellknown_record.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:convert'; - -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; - -class WellKnownRecord { - WellKnownRecord({ - required this.address, - required this.name, - }); - - final String name; - final String address; - - static Future checkWellKnownUsername(String username, CryptoCurrency currency) async { - String jsonLocation = ""; - switch (currency) { - case CryptoCurrency.nano: - jsonLocation = "nano-currency"; - break; - // TODO: add other currencies - default: - return null; - } - - // split the string by the @ symbol: - try { - final List splitStrs = username.split("@"); - String name = splitStrs.first.toLowerCase(); - final String domain = splitStrs.last; - - if (splitStrs.length == 3) { - // for username like @alice@domain.org instead of alice@domain.org - name = splitStrs[1]; - } - - if (name.isEmpty) { - name = "_"; - } - - // lookup domain/.well-known/nano-currency.json and check if it has a nano address: - final response = await ProxyWrapper().get( - clearnetUri: Uri.parse("https://$domain/.well-known/$jsonLocation.json?names=$name"), - headers: {"Accept": "application/json"}, - ); - - if (response.statusCode != 200) { - return null; - } - - final Map decoded = json.decode(response.body) as Map; - - // Access the first element in the names array and retrieve its address - final List names = decoded["names"] as List; - for (final dynamic item in names) { - if (item["name"].toLowerCase() == name) { - return item["address"] as String; - } - } - } catch (e) { - printV("error checking well-known username: $e"); - } - return null; - } - - static String formatDomainName(String name) { - String formattedName = name; - - if (name.contains("@")) { - formattedName = name.replaceAll("@", "."); - } - - return formattedName; - } - - static Future fetchAddressAndName({ - required String formattedName, - required CryptoCurrency currency, - }) async { - String name = formattedName; - - printV("formattedName: $formattedName"); - - final address = await checkWellKnownUsername(formattedName, currency); - - if (address == null) { - return null; - } - - return WellKnownRecord(address: address, name: name); - } -} diff --git a/lib/entities/yat_record.dart b/lib/entities/yat_record.dart deleted file mode 100644 index 40e677a85..000000000 --- a/lib/entities/yat_record.dart +++ /dev/null @@ -1,13 +0,0 @@ -class YatRecord { - YatRecord({ - required this.category, - required this.address, - }); - - YatRecord.fromJson(Map json) - : address = json['address'] as String, - category = json['category'] as String; - - String category; - String address; -} diff --git a/lib/entities/zano_alias.dart b/lib/entities/zano_alias.dart deleted file mode 100644 index 3d1c74969..000000000 --- a/lib/entities/zano_alias.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:convert'; - -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; - -class ZanoAlias { - static Future fetchZanoAliasAddress(String alias) async { - try { - final uri = Uri.parse("http://37.27.100.59:10500/json_rpc"); - final response = await ProxyWrapper().post( - clearnetUri: uri, - body: json.encode({ - "id": 0, - "jsonrpc": "2.0", - "method": "get_alias_details", - "params": {"alias": alias} - }), - ); - - final jsonParsed = json.decode(response.body) as Map; - - return jsonParsed['result']['alias_details']['address'] as String?; - } catch (e) { - printV('Zano Alias error: ${e.toString()}'); - } - - return null; - } -} diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index aa7cff5af..a993afbb2 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -50,6 +50,7 @@ import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/payment/payment_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; @@ -263,15 +264,34 @@ class _NewSendPageState extends State { _addressFocusNode.addListener(() async { if (!_addressFocusNode.hasFocus && _addressControllers[_selectedOutput].text.isNotEmpty) { final output = widget.sendViewModel.outputs[_selectedOutput]; - output.fetchParsedAddress(context).then((val) { - if (_addressControllers[_selectedOutput].text != output.extractedAddress) { - _addressControllers[_selectedOutput].text = output.extractedAddress; - } - }); + await _resolveAddressForOutput(output); } }); } + Future _resolveAddressForOutput(Output output) async { + final result = await widget.sendViewModel.resolveAddressForOutput(output); + if (result == null) return; + + final confirmed = await showParsedAddressConfirmationAlert(context, result); + + if (confirmed) { + output.applyAddressLookupResult(result); + } else { + output.resetParsedAddress(); + return; + } + + final outputIndex = widget.sendViewModel.outputs.indexOf(output); + if (outputIndex >= 0 && + outputIndex < _addressControllers.length && + output.extractedAddress.isNotEmpty && + _addressControllers[outputIndex].text != output.extractedAddress) { + _addressControllers[outputIndex].text = output.extractedAddress; + } + } + + @override Widget build(BuildContext context) { return Observer(builder: (_) { @@ -377,7 +397,7 @@ class _NewSendPageState extends State { focusNode: _addressFocusNode, onURIScanned: (uri) async { output.resetParsedAddress(); - await output.fetchParsedAddress(context); + await _resolveAddressForOutput(output); // Process the payment through the new flow await _handlePaymentFlow( @@ -404,7 +424,7 @@ class _NewSendPageState extends State { _justHandledPasteButton = true; try { output.resetParsedAddress(); - await output.fetchParsedAddress(context); + await _resolveAddressForOutput(output); final address = output.isParsedAddress ? output.extractedAddress @@ -1429,3 +1449,28 @@ class SendHelpPage extends StatelessWidget { ); } } + +Future showParsedAddressConfirmationAlert( + BuildContext context, + ParsedAddress parsedAddress, +) async { + final confirmed = await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).address_detected, + headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName, + headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty + ? parsedAddress.addressSource.iconPath + : parsedAddress.profileImageUrl, + alertContent: S.of(context).extracted_address_content( + '${parsedAddress.handle} (${parsedAddress.addressSource.label})', + ), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(true), + ); + }, + ); + + return confirmed ?? false; +} \ No newline at end of file diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart index 1e897743e..2120a9a1f 100644 --- a/lib/new-ui/pages/swap_page.dart +++ b/lib/new-ui/pages/swap_page.dart @@ -1,8 +1,8 @@ +import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/exchange/exchange_trade_state.dart'; import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; @@ -23,7 +23,6 @@ import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args. import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart'; import 'package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart'; -import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; @@ -52,7 +51,7 @@ import 'package:mobx/mobx.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class NewSwapPage extends StatefulWidget { - NewSwapPage(this.exchangeViewModel, this.authService, this.initialPaymentRequest, + NewSwapPage(this.exchangeViewModel, this.authService,this.adrResService, this.initialPaymentRequest, {required this.walletSwitcherViewModel, CryptoCurrency? initialCurrency}) { depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr ? exchangeViewModel.wallet.name @@ -68,6 +67,7 @@ class NewSwapPage extends StatefulWidget { final ExchangeViewModel exchangeViewModel; final WalletSwitcherViewModel walletSwitcherViewModel; final AuthService authService; + final AddressResolverService adrResService; final PaymentRequest? initialPaymentRequest; late final String? depositWalletName; late final String? receiveWalletName; @@ -423,8 +423,11 @@ class _NewSwapPageState extends State { Future fetchParsedAddress( BuildContext context, String domain, CryptoCurrency currency) async { printV("$domain"); - final parsedAddress = await getIt.get().resolve(context, domain, currency); - return extractAddressFromParsed(context, parsedAddress); + final parsedAddress = await widget.adrResService.resolve( + query: domain, + wallet: widget.exchangeViewModel.wallet, + currency: currency); + return parsedAddress.isNotEmpty ? parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? '' : ''; } void _showFeeAlert(BuildContext context) async { diff --git a/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart b/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart index 435154aa4..e284ba34a 100644 --- a/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart +++ b/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/lnurlpay_record.dart'; +import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurlpay_record.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart index e573de4f2..5234c9c3d 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart @@ -227,13 +227,14 @@ class SendTransactionDetails extends StatelessWidget { decimals: 2) : sendViewModel.pendingTransactionFeeFiatAmountFormatted; - final showAddress = !sendViewModel.outputs.any((e) => - RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) || - RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) || - (e.isParsedAddress && - e.parsedAddress.addresses.isNotEmpty && - RegExp(AddressValidator.lnurlMatcher) - .hasMatch(e.parsedAddress.addresses.firstOrNull?.toLowerCase() ?? ""))); + final showAddress = !sendViewModel.outputs.any((e) => + RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) || + RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) || + (e.isParsedAddress && + e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency] != null && + e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency]!.isNotEmpty && + RegExp(AddressValidator.lnurlMatcher) + .hasMatch(e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency]!.toLowerCase()))); final outputs = sendViewModel.outputs; diff --git a/lib/nostr/nostr_api.dart b/lib/nostr/nostr_api.dart deleted file mode 100644 index 8d9164e57..000000000 --- a/lib/nostr/nostr_api.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'dart:convert'; - -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/nostr/nostr_user.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:flutter/material.dart'; -import 'package:nostr_tools/nostr_tools.dart'; - -class NostrProfileHandler { - static final relayToDomainMap = { - 'relay.snort.social': 'snort.social', - }; - - static Nip05 _nip05 = Nip05(); - - static Future queryProfile(BuildContext context, String nip05Address) async { - var profile = await _nip05.queryProfile(nip05Address); - if (profile?.pubkey != null) { - if (profile?.relays?.isNotEmpty == true) { - return profile; - } else { - await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relays_message); - } - } - return null; - } - - static Future processRelays( - BuildContext context, - ProfilePointer profile, - String nip05Address, - ) async { - String userDomain = _extractDomain(nip05Address); - const int metaData = 0; - - for (String relayUrl in profile.relays ?? []) { - final relayDomain = _getDomainFromRelayUrl(relayUrl); - final formattedRelayDomain = relayToDomainMap[relayDomain] ?? relayDomain; - if (formattedRelayDomain == userDomain) { - final userDomainData = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaData]); - if (userDomainData != null) { - return userDomainData; - } - } - } - await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relay_on_domain); - - String? chosenRelayUrl = - await _showRelayChoiceDialog(context, profile.relays ?? []); - if (chosenRelayUrl != null) { - final userData = await _fetchInfoFromRelay(chosenRelayUrl, profile.pubkey, [metaData]); - if (userData != null) { - return userData; - } - } - - return null; - } - - static Future _fetchInfoFromRelay( - String relayUrl, String userPubKey, List kinds) async { - try { - final relay = RelayApi(relayUrl: relayUrl); - final stream = await relay.connect(); - - relay.sub([ - Filter( - kinds: kinds, - authors: [userPubKey], - ) - ]); - - await for (var message in stream) { - if (message.type == 'EVENT') { - final event = message.message as Event; - - final eventContent = json.decode(event.content) as Map; - - final userMetadata = UserMetadata.fromJson(eventContent); - relay.close(); - return userMetadata; - } - } - - relay.close(); - return null; - } catch (e) { - printV('[!] Error with relay $relayUrl: $e'); - return null; - } - } - - static Future _showErrorDialog( - BuildContext context, String title, String errorMessage) async { - if (context.mounted) { - await showPopUp( - context: context, - builder: (BuildContext dialogContext) { - return AlertWithOneAction( - alertTitle: title, - alertContent: errorMessage, - buttonText: S.of(dialogContext).ok, - buttonAction: () => Navigator.of(dialogContext).pop(), - ); - }, - ); - } - } - - static String _extractDomain(String nip05Address) { - var parts = nip05Address.split('@'); - return parts.length == 2 ? parts[1] : ''; - } - - static String _getDomainFromRelayUrl(String relayUrl) { - try { - var uri = Uri.parse(relayUrl); - return uri.host; - } catch (e) { - printV('Error parsing URL: $e'); - return ''; - } - } - - static Future _showRelayChoiceDialog( - BuildContext context, - List relays, - ) async { - String? selectedRelay; - - if (context.mounted) { - await showPopUp( - context: context, - builder: (BuildContext dialogContext) { - return Picker( - selectedAtIndex: 0, - title: S.of(dialogContext).choose_relay, - items: relays, - onItemSelected: (String relay) => selectedRelay = relay, - ); - }, - ); - } - - return selectedRelay; - } -} diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart index 73c238b57..2db9a9568 100644 --- a/lib/src/screens/buy/buy_sell_page.dart +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/buy/sell_buy_states.dart'; +import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart'; import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart'; @@ -16,7 +16,6 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart'; import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart'; -import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -28,9 +27,10 @@ import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; class BuySellPage extends BasePage { - BuySellPage(this.buySellViewModel); + BuySellPage(this.buySellViewModel, this._resolver); final BuySellViewModel buySellViewModel; + final AddressResolverService _resolver; final cryptoCurrencyKey = GlobalKey(); final fiatCurrencyKey = GlobalKey(); final _formKey = GlobalKey(); @@ -300,7 +300,7 @@ class BuySellPage extends BasePage { }); reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) { - if (cryptoAddressController != address) { + if (cryptoAddressController.text != address) { cryptoCurrencyKey.currentState!.addressController.text = address; } }); @@ -320,8 +320,10 @@ class BuySellPage extends BasePage { _cryptoAddressFocus.addListener(() async { if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) { final domain = cryptoAddressController.text; - buySellViewModel.cryptoCurrencyAddress = await fetchParsedAddress( - context, domain, buySellViewModel.cryptoCurrency); + final parsed = await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency); + if (parsed.isNotEmpty) { + buySellViewModel.cryptoCurrencyAddress = parsed; + } } }); @@ -410,8 +412,23 @@ class BuySellPage extends BasePage { currencyButtonColor: Colors.transparent, addressButtonsColor: Theme.of(context).colorScheme.surfaceContainerHighest, borderColor: Theme.of(context).colorScheme.outlineVariant, - onPushPasteButton: (context) async {}, - onPushAddressBookButton: (context) async {}, + onPushPasteButton: (context) async { + final domain = cryptoCurrencyKey.currentState!.addressController.text; + final parsed = + await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency); + if (parsed.isNotEmpty) { + buySellViewModel.cryptoCurrencyAddress = parsed; + } + }, + + onPushAddressBookButton: (context) async { + final domain = cryptoCurrencyKey.currentState!.addressController.text; + final parsed = + await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency); + if (parsed.isNotEmpty) { + buySellViewModel.cryptoCurrencyAddress = parsed; + } + }, fillColor: buySellViewModel.isBuyAction ? Theme.of(context).colorScheme.surfaceContainer : Theme.of(context).colorScheme.surfaceContainerLow, @@ -537,9 +554,10 @@ class BuySellPage extends BasePage { String domain, CryptoCurrency currency, ) async { - final parsedAddress = - await getIt.get().resolve(context, domain, currency); - final address = await extractAddressFromParsed(context, parsedAddress); - return address; + final parsedAddresses = await _resolver.resolve( + query: domain, wallet: buySellViewModel.wallet, currency: currency); + return parsedAddresses.isNotEmpty + ? parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? '' + : ''; } } diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index 07cb5625e..e60140bd5 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -340,6 +340,7 @@ class _ContactListBodyState extends State { updateFunction: widget.contactListViewModel.reorderAccordingToContactList, canReorder: widget.contactListViewModel.isEditable, shrinkWrap: true, + padding: const EdgeInsets.only(bottom: 250), itemBuilder: (context, index) { final contact = contacts[index]; final contactContent = diff --git a/lib/src/screens/contact/contact_page.dart b/lib/src/screens/contact/contact_page.dart index d5ca5fc0e..b4ea348f2 100644 --- a/lib/src/screens/contact/contact_page.dart +++ b/lib/src/screens/contact/contact_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -27,7 +28,15 @@ class ContactPage extends BasePage { _nameController.text = contactViewModel.name; _addressController.text = contactViewModel.address; _nameController.addListener(() => contactViewModel.name = _nameController.text); - _addressController.addListener(() => contactViewModel.address = _addressController.text); + _addressController.addListener(() { + final address = _addressController.text; + + if (address != contactViewModel.address) { + contactViewModel.displayName = ''; + } + + contactViewModel.address = address; + }); autorun((_) => _currencyTypeController.text = "${contactViewModel.currency?.toString() ?? ""} ${contactViewModel.currency?.tag != null ? "(${contactViewModel.currency?.tag})" : ""}"); } @@ -98,7 +107,8 @@ class ContactPage extends BasePage { focusNode: addressFocusNode, displayName: contactViewModel.displayName, addressController: _addressController, - onEditingComplete: ()=> contactViewModel.extractParsedAddress(context), + onEditingComplete: () {}, + onPushPasteButton: (context) => _extractParsedAddress(context), validator: AddressValidator(type: contactViewModel.currency!), selectedCurrency: contactViewModel.currency!, ); @@ -129,8 +139,7 @@ class ContactPage extends BasePage { builder: (_) => PrimaryButton( onPressed: () async { FocusScope.of(context).unfocus(); - - await contactViewModel.extractParsedAddress(context); + _extractParsedAddress(context); if (_formKey.currentState != null && !_formKey.currentState!.validate()) { return; @@ -150,6 +159,16 @@ class ContactPage extends BasePage { ); } + Future _extractParsedAddress(BuildContext context) async { + final parsedAddress = await contactViewModel.extractParsedAddress(context); + if (parsedAddress == null) return; + + final confirmed = await showParsedAddressConfirmationAlert(context, parsedAddress); + if (!confirmed) return; + + contactViewModel.applyParsedAddress(parsedAddress); + } + void _presentCurrencyPicker(BuildContext context) { showPopUp( builder: (_) => CurrencyPicker( @@ -203,4 +222,29 @@ class ContactPage extends BasePage { } }); } + + Future showParsedAddressConfirmationAlert( + BuildContext context, + ParsedAddress parsedAddress, + ) async { + final confirmed = await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).address_detected, + headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName, + headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty + ? parsedAddress.addressSource.iconPath + : parsedAddress.profileImageUrl, + alertContent: S.of(context).extracted_address_content( + '${parsedAddress.handle} (${parsedAddress.addressSource.label})', + ), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(true), + ); + }, + ); + + return confirmed ?? false; + } } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 40fa6c5cc..1dc2fe94f 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; @@ -11,8 +12,6 @@ import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cake_wallet/entities/parse_address_from_domain.dart'; -import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -41,7 +40,7 @@ import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; class ExchangePage extends BasePage { - ExchangePage(this.exchangeViewModel, this.authService, this.initialPaymentRequest) { + ExchangePage(this.exchangeViewModel, this.authService, this.adrResService, this.initialPaymentRequest) { depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr ? exchangeViewModel.wallet.name : null; @@ -52,6 +51,7 @@ class ExchangePage extends BasePage { final ExchangeViewModel exchangeViewModel; final AuthService authService; + final AddressResolverService adrResService; final PaymentRequest? initialPaymentRequest; final depositKey = GlobalKey(); final receiveKey = GlobalKey(); @@ -655,8 +655,8 @@ class ExchangePage extends BasePage { Future fetchParsedAddress( BuildContext context, String domain, CryptoCurrency currency) async { - final parsedAddress = await getIt.get().resolve(context, domain, currency); - return extractAddressFromParsed(context, parsedAddress); + final parsedAddresses = await adrResService.resolve(query: domain, wallet:exchangeViewModel.wallet, currency: currency); + return parsedAddresses.isNotEmpty ? parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? '' : ''; } void _showFeeAlert(BuildContext context) async { diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index d886963b2..a72aa79ae 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -780,7 +780,6 @@ class SendPage extends BasePage { } output.resetParsedAddress(); - await output.fetchParsedAddress(context); } Output _defineCurrentOutput() { diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart deleted file mode 100644 index e2564d3f2..000000000 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:cake_wallet/entities/parsed_address.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'choose_yat_address_alert.dart'; - -Future extractAddressFromParsed( - BuildContext? context, - ParsedAddress parsedAddress) async { - if (context == null || !context.mounted) return parsedAddress.addresses.first; - - var title = ''; - var content = ''; - var address = ''; - var profileImageUrl = ''; - var profileName = ''; - - switch (parsedAddress.parseFrom) { - case ParseFrom.unstoppableDomains: - title = S.of(context).address_detected; - content = S.of(context).address_from_domain(parsedAddress.name); - address = parsedAddress.addresses.first; - break; - case ParseFrom.ens: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (ENS)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.openAlias: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (OpenAlias)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.wellKnown: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (Well-Known)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.fio: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (FIO)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.twitter: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)'); - address = parsedAddress.addresses.first; - profileImageUrl = parsedAddress.profileImageUrl; - profileName = parsedAddress.profileName; - break; - case ParseFrom.mastodon: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)'); - address = parsedAddress.addresses.first; - profileImageUrl = parsedAddress.profileImageUrl; - profileName = parsedAddress.profileName; - break; - case ParseFrom.nostr: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (Nostr NIP-05)'); - address = parsedAddress.addresses.first; - profileImageUrl = parsedAddress.profileImageUrl; - profileName = parsedAddress.profileName; - break; - case ParseFrom.thorChain: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (ThorChain)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.zanoAlias: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (Zano Alias)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.zcashAddress: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (Zcash.me)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.zcashName: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (Zcash Names)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.bip353: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.lnurlpay: - title = S.of(context).address_detected; - content = S.of(context).extracted_address_content('${parsedAddress.name} (Lightning)'); - address = parsedAddress.addresses.first; - break; - case ParseFrom.yatRecord: - if (parsedAddress.name.isEmpty) { - title = S.of(context).yat_error; - content = S.of(context).yat_error_content; - address = parsedAddress.addresses.first; - break; - } - - title = S.of(context).address_detected; - content = S.of(context).address_from_yat(parsedAddress.name); - - if (parsedAddress.addresses.length == 1) { - address = parsedAddress.addresses.first; - break; - } - - content += S.of(context).choose_address; - - address = await showPopUp( - context: context, - builder: (context) => PopScope( - child: ChooseYatAddressAlert( - alertTitle: title, - alertContent: content, - addresses: parsedAddress.addresses, - ), - canPop: false, - ), - ) ?? - ''; - - if (address.isEmpty) { - return parsedAddress.name; - } - - return address; - case ParseFrom.contact: - case ParseFrom.notParsed: - return parsedAddress.addresses.first; - } - - await showPopUp( - context: context, - builder: (context) => AlertWithOneAction( - alertTitle: title, - headerTitleText: profileName.isEmpty ? null : profileName, - headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl, - alertContent: content, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ), - ); - - return address; -} diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 0472c0e52..7e4b33835 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -594,7 +594,6 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin _connectionsSyncViewModel.setLookupsTwitter(value)), - SettingsSwitcherCell( - title: 'Mastodon', - value: _connectionsSyncViewModel.looksUpMastodon, - onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsMastodon(value)), - SettingsSwitcherCell( - title: 'Yat service', - value: _connectionsSyncViewModel.looksUpYatService, - onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsYatService(value)), - SettingsSwitcherCell( - title: 'Unstoppable Domains', - value: _connectionsSyncViewModel.looksUpUnstoppableDomains, - onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsUnstoppableDomains(value)), - SettingsSwitcherCell( - title: 'OpenAlias', - value: _connectionsSyncViewModel.looksUpOpenAlias, - onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsOpenAlias(value)), - SettingsSwitcherCell( - title: 'Ethereum Name Service', - value: _connectionsSyncViewModel.looksUpENS, - onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsENS(value)), - SettingsSwitcherCell( - title: 'Zcash Names', - value: _connectionsSyncViewModel.lookupsZcashNames, - onValueChange: (_, bool value) => - _connectionsSyncViewModel.setLookupsZcashNames(value)), - SettingsSwitcherCell( - title: '.well-known', - value: _connectionsSyncViewModel.looksUpWellKnown, - onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsWellKnown(value)), - SettingsSwitcherCell( - title: 'Zano Aliases', - value: _connectionsSyncViewModel.lookupsZanoAlias, - onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsZanoAlias(value)), - - //if (!isHaven) it does not work correctly - ], + children: _connectionsSyncViewModel.domainLookupSources + .map( + (source) => SettingsSwitcherCell( + title: source.label, + leading: source.iconPath.isNotEmpty ? CakeImageWidget( + imageUrl: source.iconPath, + width: 24, + height: 24, + ) : SizedBox(width: 24, height: 24), + value: _connectionsSyncViewModel.lookupValue(source), + onValueChange: (_, bool value) => + _connectionsSyncViewModel.setLookupValue(source, value), + ), + ) + .toList(), ), ); }), diff --git a/lib/src/screens/wallet_list/filtered_list.dart b/lib/src/screens/wallet_list/filtered_list.dart index 86368210c..5ec27cec5 100644 --- a/lib/src/screens/wallet_list/filtered_list.dart +++ b/lib/src/screens/wallet_list/filtered_list.dart @@ -10,6 +10,7 @@ class FilteredList extends StatefulWidget { this.canReorder = true, this.shrinkWrap = false, this.physics, + this.padding, }); final ObservableList list; @@ -18,6 +19,7 @@ class FilteredList extends StatefulWidget { final bool canReorder; final bool shrinkWrap; final ScrollPhysics? physics; + final EdgeInsets? padding; @override FilteredListState createState() => FilteredListState(); @@ -48,6 +50,7 @@ class FilteredListState extends State { ), shrinkWrap: widget.shrinkWrap, physics: widget.physics ?? const BouncingScrollPhysics(), + padding: widget.padding, itemBuilder: widget.itemBuilder, itemCount: widget.list.length, onReorder: (int oldIndex, int newIndex) { diff --git a/lib/src/screens/yat/yat_popup.dart b/lib/src/screens/yat/yat_popup.dart index 33c94a463..bcf17a4f1 100644 --- a/lib/src/screens/yat/yat_popup.dart +++ b/lib/src/screens/yat/yat_popup.dart @@ -1,10 +1,10 @@ +import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart'; import 'package:cake_wallet/src/screens/yat/widgets/first_introduction.dart'; import 'package:cake_wallet/src/screens/yat/widgets/second_introduction.dart'; import 'package:cake_wallet/src/screens/yat/widgets/third_introduction.dart'; import 'package:cake_wallet/src/screens/yat/widgets/yat_close_button.dart'; import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; import 'package:animate_do/animate_do.dart'; diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index e98ae1c07..25c08ff6a 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:flutter/material.dart'; @@ -184,10 +185,7 @@ class BaseAlertDialog extends StatelessWidget { radius: 50, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, child: ClipOval( - child: Image.network( - imageUrl, - fit: BoxFit.cover, - ), + child: CakeImageWidget (imageUrl: imageUrl, width: 100, height: 100, fit: BoxFit.cover), ), ), ); diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index 140cfa970..965365285 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -163,7 +163,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { itemBuilder: (context, index) { final isBatchSending = outputs.length > 1; final item = outputs[index]; - final contactName = item.parsedAddress.name; + final contactName = item.parsedAddress.profileName; final isCakePayName = contactName == 'Cake Pay'; final batchContactTitle = '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}'; diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 732ca03c9..b06e2acb6 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -132,7 +132,13 @@ abstract class SettingsStoreBase with Store { required this.lookupsOpenAlias, required this.lookupsENS, required this.lookupsZcashNames, + required this.lookupsZcashAddress, required this.lookupsWellKnown, + required this.lookupsFio, + required this.lookupsNostr, + required this.lookupsThorChain, + required this.lookupsBip353, + required this.lookupsLNUrl, required this.usePayjoin, required this.showPayjoinCard, required this.customBitcoinFeeRate, @@ -567,11 +573,41 @@ abstract class SettingsStoreBase with Store { (bool looksUpZcashNames) => _sharedPreferences.setBool( PreferencesKey.lookupsZcashNames, looksUpZcashNames)); + reaction( + (_) => lookupsZcashAddress, + (bool lookupsZcashAddress) => _sharedPreferences.setBool( + PreferencesKey.lookupsZcashAddress, lookupsZcashAddress)); + reaction( (_) => lookupsWellKnown, (bool looksUpWellKnown) => _sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown)); + reaction( + (_) => lookupsFio, + (bool lookupsFio) => + _sharedPreferences.setBool(PreferencesKey.lookupsFio, lookupsFio)); + + reaction( + (_) => lookupsNostr, + (bool lookupsNostr) => + _sharedPreferences.setBool(PreferencesKey.lookupsNostr, lookupsNostr)); + + reaction( + (_) => lookupsThorChain, + (bool lookupsThorChain) => + _sharedPreferences.setBool(PreferencesKey.lookupsThorChain, lookupsThorChain)); + + reaction( + (_) => lookupsBip353, + (bool lookupsBip353) => + _sharedPreferences.setBool(PreferencesKey.lookupsBip353, lookupsBip353)); + + reaction( + (_) => lookupsLNUrl, + (bool lookupsLNUrl) => + _sharedPreferences.setBool(PreferencesKey.lookupsLNUrl, lookupsLNUrl)); + reaction((_) => usePayjoin, (bool usePayjoin) => _sharedPreferences.setBool(PreferencesKey.usePayjoin, usePayjoin)); @@ -962,9 +998,27 @@ abstract class SettingsStoreBase with Store { @observable bool lookupsZcashNames; + @observable + bool lookupsZcashAddress; + @observable bool lookupsWellKnown; + @observable + bool lookupsFio; + + @observable + bool lookupsNostr; + + @observable + bool lookupsThorChain; + + @observable + bool lookupsBip353; + + @observable + bool lookupsLNUrl; + @observable bool usePayjoin; @@ -1268,9 +1322,14 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; - final lookupsZcashNames = - sharedPreferences.getBool(PreferencesKey.lookupsZcashNames) ?? true; + final lookupsZcashNames = sharedPreferences.getBool(PreferencesKey.lookupsZcashNames) ?? true; + final lookupsZcashAddress = sharedPreferences.getBool(PreferencesKey.lookupsZcashAddress) ?? true; final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true; + final lookupsFio = sharedPreferences.getBool(PreferencesKey.lookupsFio) ?? true; + final lookupsNostr = sharedPreferences.getBool(PreferencesKey.lookupsNostr) ?? true; + final lookupsThorChain = sharedPreferences.getBool(PreferencesKey.lookupsThorChain) ?? false; + final lookupsBip353 = sharedPreferences.getBool(PreferencesKey.lookupsBip353) ?? true; + final lookupsLNUrl = sharedPreferences.getBool(PreferencesKey.lookupsLNUrl) ?? true; final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false; final showPayjoinCard = sharedPreferences.getBool(PreferencesKey.showPayjoinCard) ?? true; final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1; @@ -1659,7 +1718,13 @@ abstract class SettingsStoreBase with Store { lookupsOpenAlias: lookupsOpenAlias, lookupsENS: lookupsENS, lookupsZcashNames: lookupsZcashNames, + lookupsZcashAddress: lookupsZcashAddress, lookupsWellKnown: lookupsWellKnown, + lookupsFio: lookupsFio, + lookupsNostr: lookupsNostr, + lookupsThorChain: lookupsThorChain, + lookupsBip353: lookupsBip353, + lookupsLNUrl: lookupsLNUrl, usePayjoin: usePayjoin, showPayjoinCard: showPayjoinCard, customBitcoinFeeRate: customBitcoinFeeRate, diff --git a/lib/store/yat/yat_exception.dart b/lib/store/yat/yat_exception.dart deleted file mode 100644 index 71313ba88..000000000 --- a/lib/store/yat/yat_exception.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class YatException implements Exception { - YatException({required this.text}); - - final String text; - - @override - String toString() => 'Yat exception: $text'; -} \ No newline at end of file diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index c808f966c..d0d74b953 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -1,7 +1,10 @@ +import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact_record.dart'; -import 'package:cake_wallet/entities/parse_address_from_domain.dart'; -import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -16,7 +19,8 @@ part 'contact_view_model.g.dart'; class ContactViewModel = ContactViewModelBase with _$ContactViewModel; abstract class ContactViewModelBase with Store { - ContactViewModelBase(this._contacts, {ContactRecord? contact}) + ContactViewModelBase(this._contacts, this.appStore,this.adrResService, + {ContactRecord? contact}) : state = InitialExecutionState(), currencies = CryptoCurrency.all, _contact = contact, @@ -26,6 +30,9 @@ abstract class ContactViewModelBase with Store { currency = contact?.type, lastChange = contact?.lastChange; + final AppStore appStore; + final AddressResolverService adrResService; + @observable ExecutionState state; @@ -58,16 +65,52 @@ abstract class ContactViewModelBase with Store { void reset() { address = ''; name = ''; + displayName = ''; currency = null; } - Future extractParsedAddress(BuildContext context) async{ - if(currency == null) return; - final parsedAddress = await getIt.get().resolve(context, address, currency!); - if(parsedAddress.name.isNotEmpty) { - displayName = parsedAddress.name; + Future extractParsedAddress(BuildContext context) async { + final wallet = appStore.wallet; + final currentCurrency = currency; + final query = address.trim(); + + if (wallet == null) return null; + if (currentCurrency == null) return null; + if (query.isEmpty) return null; + + + // Check if the address is valid for the current currency (except for Zano, which can use handles as addresses) + if (currentCurrency != CryptoCurrency.zano) { + final isValidAddress = AddressValidator(type: currentCurrency).isValid(query); + if (isValidAddress) return null; } - address = await extractAddressFromParsed(null, parsedAddress); + + final parsedAddresses = await adrResService.resolve( + query: query, + wallet: wallet, + currency: currentCurrency, + ); + + if (parsedAddresses.isEmpty) return null; + + final resolvedAddress = parsedAddresses.first.parsedAddressByCurrencyMap[currentCurrency]; + if (resolvedAddress == null || resolvedAddress.isEmpty) return null; + + return parsedAddresses.first; + } + + @action + void applyParsedAddress(ParsedAddress parsedAddress) { + final currentCurrency = currency; + if (currentCurrency == null) return; + + final resolvedAddress = parsedAddress.parsedAddressByCurrencyMap[currentCurrency]; + if (resolvedAddress == null || resolvedAddress.isEmpty) return; + + address = resolvedAddress; + displayName = parsedAddress.profileName.isNotEmpty + ? parsedAddress.profileName + : parsedAddress.handle; } Future save() async { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index d97d119a4..f9d355c5a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -4,6 +4,7 @@ import 'dart:io' show Platform; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart'; import 'package:cake_wallet/core/key_service.dart'; import "package:cw_core/balance_card_style_settings.dart"; import 'package:cake_wallet/core/trade_monitor.dart'; @@ -35,7 +36,6 @@ import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 4217aab59..732729d6c 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -1,7 +1,7 @@ import 'dart:convert'; +import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart'; import 'package:cake_wallet/core/seed_validator.dart'; -import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; @@ -87,7 +87,7 @@ class WalletRestoreFromQRCode { static String? _extractAddressFromUrl(String rawString, WalletType type) { try { - return AddressResolver.extractAddressByType( + return AddressResolverUtils.extractAddressByType( raw: rawString, type: walletTypeToCryptoCurrency( type, diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 543a541bd..ef4609386 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,16 +1,16 @@ import 'dart:math' show min; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/address_resolver/address_sources.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/contact_base.dart'; -import 'package:cake_wallet/entities/parse_address_from_domain.dart'; -import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -37,7 +37,8 @@ class Output = OutputBase with _$Output; abstract class OutputBase with Store { OutputBase(this._wallet, this._appStore, this._fiatConversationStore, this.cryptoCurrencyHandler) - : key = UniqueKey(), + : + key = UniqueKey(), sendAll = false, cryptoAmount = '', cryptoFullBalance = '', @@ -47,7 +48,7 @@ abstract class OutputBase with Store { memo = "", extractedAddress = '', estimatedFee = Money.zero(cryptoCurrencyHandler()), - parsedAddress = ParsedAddress(addresses: []) { + parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) { autorun((_) { final status = _wallet.syncStatus; printV("Sync status changed to $status. Recalculating fees"); @@ -56,6 +57,7 @@ abstract class OutputBase with Store { }); } + Key key; bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler()); @@ -83,7 +85,7 @@ abstract class OutputBase with Store { @observable String note; - + @observable String memo; @@ -99,7 +101,7 @@ abstract class OutputBase with Store { @computed bool get isParsedAddress => - parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; + parsedAddress.addressSource != AddressSource.notParsed && parsedAddress.handle.isNotEmpty; String roundedCryptoAmount(int digits) => displayCryptoAmount.withMaxDecimals(digits); @@ -272,10 +274,22 @@ abstract class OutputBase with Store { resetParsedAddress(); } + @action void resetParsedAddress() { displayName = null; extractedAddress = ''; - parsedAddress = ParsedAddress(addresses: []); + note = ''; + parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}); + } + + @action + void applyAddressLookupResult(ParsedAddress result) { + final currency = cryptoCurrencyHandler(); + + parsedAddress = result; + extractedAddress = result.parsedAddressByCurrencyMap[currency] ?? ''; + note = result.description; + displayName = result.profileName.isNotEmpty ? result.profileName : result.handle; } @action @@ -334,33 +348,26 @@ abstract class OutputBase with Store { Map get extra { final fields = {}; - if (parsedAddress.parseFrom == ParseFrom.bip353) { - fields['bip353_name'] = parsedAddress.name; + if (parsedAddress.addressSource == AddressSource.bip353) { + fields['bip353_name'] = parsedAddress.handle; fields['bip353_proof'] = parsedAddress.bip353DnsProof; } return fields; } - Future fetchParsedAddress(BuildContext context) async { - final domain = address; - final currency = cryptoCurrencyHandler(); - parsedAddress = await getIt.get().resolve(context, domain, currency); - if(parsedAddress.name.isNotEmpty) { - displayName = parsedAddress.name; - } - - if (parsedAddress.parseFrom == ParseFrom.lnurlpay) { - cryptoCurrencyHandler(CryptoCurrency.btcln); - } - extractedAddress = await extractAddressFromParsed(context, parsedAddress); - note = parsedAddress.description; - } - + @action void loadContact(ContactBase contact) { - address = contact.name; - parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name); - extractedAddress = parsedAddress.addresses.first; - note = parsedAddress.description; + final currency = cryptoCurrencyHandler(); + + address = contact.address; + applyAddressLookupResult( + ParsedAddress( + parsedAddressByCurrencyMap: {currency: contact.address}, + addressSource: AddressSource.contact, + handle: contact.name, + profileName: contact.name, + ), + ); } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 5c4b2c09c..61990c195 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,6 +1,9 @@ import 'dart:async'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; +import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/amount_validator.dart'; @@ -17,7 +20,6 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; -import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -103,6 +105,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this._appStore, this.sendTemplateViewModel, this._fiatConversationStore, + this._adrResService, this.balanceViewModel, this.contactListViewModel, this.transactionDescriptionBox, @@ -182,6 +185,26 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (isBatchSending) outputs.remove(output); } + Future resolveAddressForOutput(Output output) async { + final query = output.address.trim(); + if (query.isEmpty) return null; + + // Check if the address is valid for the current currency (except for Zano, which can use handles as addresses) + if (selectedCryptoCurrency != CryptoCurrency.zano) { + final isValidAddress = AddressValidator(type: selectedCryptoCurrency).isValid(query); + if (isValidAddress) return null; + } + + + final results = await _adrResService.resolve( + query: query, + wallet: wallet, + currency: selectedCryptoCurrency, + ); + + return results.isEmpty ? null : results.first; + } + @action void clearOutputs() { outputs.clear(); @@ -383,14 +406,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // If silent payments scanning, can still send payments (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus); - bool isSendToSilentPayments(Output output) => - wallet.type == WalletType.bitcoin && - (RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(output.address) || - RegExp(AddressValidator.silentPaymentAddressPatternMainnet) - .hasMatch(output.extractedAddress) || - (output.parsedAddress.addresses.isNotEmpty && - RegExp(AddressValidator.silentPaymentAddressPatternMainnet) - .hasMatch(output.parsedAddress.addresses[0]))); + bool isSendToSilentPayments(Output output) { + if (wallet.type != WalletType.bitcoin) return false; + + final sp = RegExp(AddressValidator.silentPaymentAddressPatternMainnet); + + final address = output.extractedAddress.isNotEmpty + ? output.extractedAddress + : output.address; + + return sp.hasMatch(address); + } @computed List