diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 36fc3d65f..0eea5a85d 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -9,6 +9,7 @@ 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'; @@ -458,6 +459,16 @@ class AddressResolver { } } + 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); diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index 329e83536..28bdf704f 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -16,6 +16,7 @@ enum ParseFrom { wellKnown, zanoAlias, zcashAddress, + zcashName, bip353, lnurlpay, } @@ -177,6 +178,14 @@ class ParsedAddress { ); } + 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], diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 9e30e2eb8..be90ed6ab 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -101,6 +101,7 @@ class PreferencesKey { static const lookupsUnstoppableDomains = 'looks_up_unstoppable_domain'; static const lookupsOpenAlias = 'looks_up_open_alias'; static const lookupsENS = 'looks_up_ens'; + static const lookupsZcashNames = 'looks_up_zcash_names'; static const lookupsWellKnown = 'looks_up_well_known'; static const useBlinkProtection = 'use_blink_protection'; static const usePayjoin = 'use_payjoin'; diff --git a/lib/entities/zcash_names_record.dart b/lib/entities/zcash_names_record.dart new file mode 100644 index 000000000..1a20a472b --- /dev/null +++ b/lib/entities/zcash_names_record.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; + +class ZcashNamesRecord { + static final Uri _mainnetEndpoint = Uri.parse('https://main.zcashnames.com'); + + static final RegExp _nameRegExp = RegExp(r'^[A-Za-z0-9_-]{1,63}$'); + + static Future fetchZcashNamesAddress(String input) async { + final name = _extractName(input); + if (name == null) return null; + + try { + final response = await ProxyWrapper().post( + clearnetUri: _mainnetEndpoint, + headers: const {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'jsonrpc': '2.0', + 'id': 1, + 'method': 'resolve', + 'params': {'query': name}, + }), + ); + + if (response.statusCode != 200) { + printV('ZcashNames: non-200 status ${response.statusCode} for $name'); + return null; + } + + final decoded = jsonDecode(response.body); + if (decoded is! Map) return null; + if (decoded['error'] != null) return null; + + final result = decoded['result']; + if (result is! Map) return null; + + final address = result['address']; + if (address is! String || address.isEmpty) return null; + + if (!_isValidZcashAddress(address)) { + printV('ZcashNames: returned address failed validation for $name'); + return null; + } + return address; + } catch (e) { + printV('ZcashNames: lookup failed for $name: $e'); + return null; + } + } + + static String? _extractName(String input) { + final lower = input.toLowerCase().trim(); + String? raw; + if (lower.endsWith('.zcash')) { + raw = lower.substring(0, lower.length - '.zcash'.length); + } else if (lower.endsWith('.zec')) { + raw = lower.substring(0, lower.length - '.zec'.length); + } + if (raw == null || raw.isEmpty) return null; + if (!_nameRegExp.hasMatch(raw)) return null; + return raw; + } + + static bool _isValidZcashAddress(String address) { + final pattern = + AddressValidator.getAddressFromStringPattern(CryptoCurrency.zec); + if (pattern == null) return false; + return RegExp('^(?:$pattern)\$').hasMatch(address); + } +} diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart index 1d05a4fa9..e9b6237b9 100644 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -256,28 +256,33 @@ class ThorChainExchangeProvider extends ExchangeProvider { static Future?>? lookupAddressByName(String name) async { final uri = Uri.https(_baseURL, '$_nameLookUpPath$name'); - final response = await ProxyWrapper().get(clearnetUri: uri); - - if (response.statusCode != 200) { - return null; - } - - final body = json.decode(response.body) as Map; - final entries = body['entries'] as List?; - - if (entries == null || entries.isEmpty) { - return null; - } - - Map chainToAddressMap = {}; - - for (final entry in entries) { - final chain = entry['chain'] as String; - final address = entry['address'] as String; - chainToAddressMap[chain] = address; - } - - return chainToAddressMap; + try { + final response = await ProxyWrapper().get(clearnetUri: uri); + + if (response.statusCode != 200) { + return null; + } + + final body = json.decode(response.body) as Map; + final entries = body['entries'] as List?; + + if (entries == null || entries.isEmpty) { + return null; + } + + Map chainToAddressMap = {}; + + for (final entry in entries) { + final chain = entry['chain'] as String; + final address = entry['address'] as String; + chainToAddressMap[chain] = address; + } + + return chainToAddressMap; +} catch (e) { + printV(e.toString()); + return null; +} } Future> _getSwapQuote(Map params) async { diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index ae08d5878..e2564d3f2 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -78,6 +78,11 @@ Future extractAddressFromParsed( 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)'); diff --git a/lib/src/screens/settings/domain_lookups_page.dart b/lib/src/screens/settings/domain_lookups_page.dart index b566892aa..9240af612 100644 --- a/lib/src/screens/settings/domain_lookups_page.dart +++ b/lib/src/screens/settings/domain_lookups_page.dart @@ -45,6 +45,11 @@ class DomainLookupsPage extends BasePage { 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, diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index fb14beae4..9551942e7 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -131,6 +131,7 @@ abstract class SettingsStoreBase with Store { required this.lookupsUnstoppableDomains, required this.lookupsOpenAlias, required this.lookupsENS, + required this.lookupsZcashNames, required this.lookupsWellKnown, required this.usePayjoin, required this.showPayjoinCard, @@ -561,6 +562,11 @@ abstract class SettingsStoreBase with Store { reaction((_) => lookupsENS, (bool looksUpENS) => _sharedPreferences.setBool(PreferencesKey.lookupsENS, looksUpENS)); + reaction( + (_) => lookupsZcashNames, + (bool looksUpZcashNames) => _sharedPreferences.setBool( + PreferencesKey.lookupsZcashNames, looksUpZcashNames)); + reaction( (_) => lookupsWellKnown, (bool looksUpWellKnown) => @@ -953,6 +959,9 @@ abstract class SettingsStoreBase with Store { @observable bool lookupsENS; + @observable + bool lookupsZcashNames; + @observable bool lookupsWellKnown; @@ -1260,6 +1269,8 @@ 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 lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true; final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false; final showPayjoinCard = sharedPreferences.getBool(PreferencesKey.showPayjoinCard) ?? true; @@ -1625,6 +1636,7 @@ abstract class SettingsStoreBase with Store { lookupsUnstoppableDomains: lookupsUnstoppableDomains, lookupsOpenAlias: lookupsOpenAlias, lookupsENS: lookupsENS, + lookupsZcashNames: lookupsZcashNames, lookupsWellKnown: lookupsWellKnown, usePayjoin: usePayjoin, showPayjoinCard: showPayjoinCard, @@ -1851,6 +1863,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; + lookupsZcashNames = + sharedPreferences.getBool(PreferencesKey.lookupsZcashNames) ?? true; lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true; customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1; silentPaymentsCardDisplay = diff --git a/lib/view_model/settings/connection_sync_view_model.dart b/lib/view_model/settings/connection_sync_view_model.dart index b48ae3230..47f4be7f4 100644 --- a/lib/view_model/settings/connection_sync_view_model.dart +++ b/lib/view_model/settings/connection_sync_view_model.dart @@ -6,7 +6,6 @@ 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/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -50,6 +49,9 @@ abstract class ConnectionSyncViewModelBase with Store { @computed bool get looksUpENS => _settingsStore.lookupsENS; + @computed + bool get lookupsZcashNames => _settingsStore.lookupsZcashNames; + @computed bool get looksUpWellKnown => _settingsStore.lookupsWellKnown; @@ -129,6 +131,9 @@ abstract class ConnectionSyncViewModelBase with Store { @action void setLookupsENS(bool value) => _settingsStore.lookupsENS = value; + @action + void setLookupsZcashNames(bool value) => _settingsStore.lookupsZcashNames = value; + @action void setLookupsWellKnown(bool value) => _settingsStore.lookupsWellKnown = value; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index c91937da7..b920a21e7 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -99,6 +99,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get looksUpENS => _settingsStore.lookupsENS; + @computed + bool get lookupsZcashNames => _settingsStore.lookupsZcashNames; + @computed bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;