feat: add support for Zcash names resolution (#3237)

* feat: Add memo support for swap * fix: Error on swap page select receiver bottomsheet when picking receiveing currency that's not a wallet type * feat: exclude providers that do not support memo when receive currency needs it, also show passed memo in confirmation and trade history sheets * fix: overflow for destination tag on swap confirmation * feat: add support for Zcash names resolution * fix conflict because github is shit --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

David Adegoke committed May 24, 2026 at 12:36 UTC baac6eeabc10b8ab9aef09ec274a71a76a7a55ad
10 files changed +156 -23
lib/entities/parse_address_from_domain.dart
+11
@@ -9,6 +9,7 @@ import 'package:cake_wallet/entities/parsed_address.dart';
9 import 'package:cake_wallet/entities/unstoppable_domain_address.dart';
10 import 'package:cake_wallet/entities/wellknown_record.dart';
11 import 'package:cake_wallet/entities/zano_alias.dart';
12 +import 'package:cake_wallet/entities/zcash_names_record.dart';
13 import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
14 import 'package:cake_wallet/mastodon/mastodon_api.dart';
15 import 'package:cake_wallet/nostr/nostr_api.dart';
@@ -458,6 +459,16 @@ class AddressResolver {
459 }
460 }
461
462 + final lowerText = text.toLowerCase();
463 + if (lowerText.endsWith(".zec") || lowerText.endsWith(".zcash")) {
464 + if (settingsStore.lookupsZcashNames) {
465 + final address = await ZcashNamesRecord.fetchZcashNamesAddress(text);
466 + if (address != null && address.isNotEmpty) {
467 + return ParsedAddress.zcashNameAddress(address: address, name: text);
468 + }
469 + }
470 + }
471 +
472 if (text.endsWith(".eth")) {
473 if (settingsStore.lookupsENS) {
474 final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet);
lib/entities/parsed_address.dart
+9
@@ -16,6 +16,7 @@ enum ParseFrom {
16 wellKnown,
17 zanoAlias,
18 zcashAddress,
19 + zcashName,
20 bip353,
21 lnurlpay,
22 }
@@ -177,6 +178,14 @@ class ParsedAddress {
178 );
179 }
180
181 + factory ParsedAddress.zcashNameAddress({required String address, required String name}) {
182 + return ParsedAddress(
183 + addresses: [address],
184 + name: name,
185 + parseFrom: ParseFrom.zcashName,
186 + );
187 + }
188 +
189 factory ParsedAddress.fetchWellKnownAddress({required String address, required String name}) {
190 return ParsedAddress(
191 addresses: [address],
lib/entities/preferences_key.dart
+1
@@ -101,6 +101,7 @@ class PreferencesKey {
101 static const lookupsUnstoppableDomains = 'looks_up_unstoppable_domain';
102 static const lookupsOpenAlias = 'looks_up_open_alias';
103 static const lookupsENS = 'looks_up_ens';
104 + static const lookupsZcashNames = 'looks_up_zcash_names';
105 static const lookupsWellKnown = 'looks_up_well_known';
106 static const useBlinkProtection = 'use_blink_protection';
107 static const usePayjoin = 'use_payjoin';
lib/entities/zcash_names_record.dart new
+75
@@ -0,0 +1,75 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +
4 +import 'package:cake_wallet/core/address_validator.dart';
5 +import 'package:cw_core/crypto_currency.dart';
6 +import 'package:cw_core/utils/print_verbose.dart';
7 +import 'package:cw_core/utils/proxy_wrapper.dart';
8 +
9 +class ZcashNamesRecord {
10 + static final Uri _mainnetEndpoint = Uri.parse('https://main.zcashnames.com');
11 +
12 + static final RegExp _nameRegExp = RegExp(r'^[A-Za-z0-9_-]{1,63}$');
13 +
14 + static Future<String?> fetchZcashNamesAddress(String input) async {
15 + final name = _extractName(input);
16 + if (name == null) return null;
17 +
18 + try {
19 + final response = await ProxyWrapper().post(
20 + clearnetUri: _mainnetEndpoint,
21 + headers: const {'Content-Type': 'application/json'},
22 + body: jsonEncode({
23 + 'jsonrpc': '2.0',
24 + 'id': 1,
25 + 'method': 'resolve',
26 + 'params': {'query': name},
27 + }),
28 + );
29 +
30 + if (response.statusCode != 200) {
31 + printV('ZcashNames: non-200 status ${response.statusCode} for $name');
32 + return null;
33 + }
34 +
35 + final decoded = jsonDecode(response.body);
36 + if (decoded is! Map<String, dynamic>) return null;
37 + if (decoded['error'] != null) return null;
38 +
39 + final result = decoded['result'];
40 + if (result is! Map<String, dynamic>) return null;
41 +
42 + final address = result['address'];
43 + if (address is! String || address.isEmpty) return null;
44 +
45 + if (!_isValidZcashAddress(address)) {
46 + printV('ZcashNames: returned address failed validation for $name');
47 + return null;
48 + }
49 + return address;
50 + } catch (e) {
51 + printV('ZcashNames: lookup failed for $name: $e');
52 + return null;
53 + }
54 + }
55 +
56 + static String? _extractName(String input) {
57 + final lower = input.toLowerCase().trim();
58 + String? raw;
59 + if (lower.endsWith('.zcash')) {
60 + raw = lower.substring(0, lower.length - '.zcash'.length);
61 + } else if (lower.endsWith('.zec')) {
62 + raw = lower.substring(0, lower.length - '.zec'.length);
63 + }
64 + if (raw == null || raw.isEmpty) return null;
65 + if (!_nameRegExp.hasMatch(raw)) return null;
66 + return raw;
67 + }
68 +
69 + static bool _isValidZcashAddress(String address) {
70 + final pattern =
71 + AddressValidator.getAddressFromStringPattern(CryptoCurrency.zec);
72 + if (pattern == null) return false;
73 + return RegExp('^(?:$pattern)\$').hasMatch(address);
74 + }
75 +}
lib/exchange/provider/thorchain_exchange.provider.dart
+27 -22
@@ -256,28 +256,33 @@ class ThorChainExchangeProvider extends ExchangeProvider {
256
257 static Future<Map<String, String>?>? lookupAddressByName(String name) async {
258 final uri = Uri.https(_baseURL, '$_nameLookUpPath$name');
259 - final response = await ProxyWrapper().get(clearnetUri: uri);
260 -
261 - if (response.statusCode != 200) {
262 - return null;
263 - }
264 -
265 - final body = json.decode(response.body) as Map<String, dynamic>;
266 - final entries = body['entries'] as List<dynamic>?;
267 -
268 - if (entries == null || entries.isEmpty) {
269 - return null;
270 - }
271 -
272 - Map<String, String> chainToAddressMap = {};
273 -
274 - for (final entry in entries) {
275 - final chain = entry['chain'] as String;
276 - final address = entry['address'] as String;
277 - chainToAddressMap[chain] = address;
278 - }
279 -
280 - return chainToAddressMap;
259 + try {
260 + final response = await ProxyWrapper().get(clearnetUri: uri);
261 +
262 + if (response.statusCode != 200) {
263 + return null;
264 + }
265 +
266 + final body = json.decode(response.body) as Map<String, dynamic>;
267 + final entries = body['entries'] as List<dynamic>?;
268 +
269 + if (entries == null || entries.isEmpty) {
270 + return null;
271 + }
272 +
273 + Map<String, String> chainToAddressMap = {};
274 +
275 + for (final entry in entries) {
276 + final chain = entry['chain'] as String;
277 + final address = entry['address'] as String;
278 + chainToAddressMap[chain] = address;
279 + }
280 +
281 + return chainToAddressMap;
282 +} catch (e) {
283 + printV(e.toString());
284 + return null;
285 +}
286 }
287
288 Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async {
lib/src/screens/send/widgets/extract_address_from_parsed.dart
+5
@@ -78,6 +78,11 @@ Future<String> extractAddressFromParsed(
78 content = S.of(context).extracted_address_content('${parsedAddress.name} (Zcash.me)');
79 address = parsedAddress.addresses.first;
80 break;
81 + case ParseFrom.zcashName:
82 + title = S.of(context).address_detected;
83 + content = S.of(context).extracted_address_content('${parsedAddress.name} (Zcash Names)');
84 + address = parsedAddress.addresses.first;
85 + break;
86 case ParseFrom.bip353:
87 title = S.of(context).address_detected;
88 content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)');
lib/src/screens/settings/domain_lookups_page.dart
+5
@@ -45,6 +45,11 @@ class DomainLookupsPage extends BasePage {
45 title: 'Ethereum Name Service',
46 value: _connectionsSyncViewModel.looksUpENS,
47 onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsENS(value)),
48 + SettingsSwitcherCell(
49 + title: 'Zcash Names',
50 + value: _connectionsSyncViewModel.lookupsZcashNames,
51 + onValueChange: (_, bool value) =>
52 + _connectionsSyncViewModel.setLookupsZcashNames(value)),
53 SettingsSwitcherCell(
54 title: '.well-known',
55 value: _connectionsSyncViewModel.looksUpWellKnown,
lib/store/settings_store.dart
+14
@@ -131,6 +131,7 @@ abstract class SettingsStoreBase with Store {
131 required this.lookupsUnstoppableDomains,
132 required this.lookupsOpenAlias,
133 required this.lookupsENS,
134 + required this.lookupsZcashNames,
135 required this.lookupsWellKnown,
136 required this.usePayjoin,
137 required this.showPayjoinCard,
@@ -561,6 +562,11 @@ abstract class SettingsStoreBase with Store {
562 reaction((_) => lookupsENS,
563 (bool looksUpENS) => _sharedPreferences.setBool(PreferencesKey.lookupsENS, looksUpENS));
564
565 + reaction(
566 + (_) => lookupsZcashNames,
567 + (bool looksUpZcashNames) => _sharedPreferences.setBool(
568 + PreferencesKey.lookupsZcashNames, looksUpZcashNames));
569 +
570 reaction(
571 (_) => lookupsWellKnown,
572 (bool looksUpWellKnown) =>
@@ -953,6 +959,9 @@ abstract class SettingsStoreBase with Store {
959 @observable
960 bool lookupsENS;
961
962 + @observable
963 + bool lookupsZcashNames;
964 +
965 @observable
966 bool lookupsWellKnown;
967
@@ -1260,6 +1269,8 @@ abstract class SettingsStoreBase with Store {
1269 sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true;
1270 final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
1271 final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
1272 + final lookupsZcashNames =
1273 + sharedPreferences.getBool(PreferencesKey.lookupsZcashNames) ?? true;
1274 final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
1275 final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false;
1276 final showPayjoinCard = sharedPreferences.getBool(PreferencesKey.showPayjoinCard) ?? true;
@@ -1625,6 +1636,7 @@ abstract class SettingsStoreBase with Store {
1636 lookupsUnstoppableDomains: lookupsUnstoppableDomains,
1637 lookupsOpenAlias: lookupsOpenAlias,
1638 lookupsENS: lookupsENS,
1639 + lookupsZcashNames: lookupsZcashNames,
1640 lookupsWellKnown: lookupsWellKnown,
1641 usePayjoin: usePayjoin,
1642 showPayjoinCard: showPayjoinCard,
@@ -1851,6 +1863,8 @@ abstract class SettingsStoreBase with Store {
1863 sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true;
1864 lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
1865 lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
1866 + lookupsZcashNames =
1867 + sharedPreferences.getBool(PreferencesKey.lookupsZcashNames) ?? true;
1868 lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
1869 customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
1870 silentPaymentsCardDisplay =
lib/view_model/settings/connection_sync_view_model.dart
+6 -1
@@ -6,7 +6,6 @@ import 'package:cake_wallet/evm/evm.dart';
6 import 'package:cake_wallet/generated/i18n.dart';
7 import 'package:cake_wallet/reactions/wallet_connect.dart';
8 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
9 -import 'package:cake_wallet/store/app_store.dart';
9 import 'package:cake_wallet/store/settings_store.dart';
10 import 'package:cake_wallet/tron/tron.dart';
11 import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -50,6 +49,9 @@ abstract class ConnectionSyncViewModelBase with Store {
49 @computed
50 bool get looksUpENS => _settingsStore.lookupsENS;
51
52 + @computed
53 + bool get lookupsZcashNames => _settingsStore.lookupsZcashNames;
54 +
55 @computed
56 bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
57
@@ -129,6 +131,9 @@ abstract class ConnectionSyncViewModelBase with Store {
131 @action
132 void setLookupsENS(bool value) => _settingsStore.lookupsENS = value;
133
134 + @action
135 + void setLookupsZcashNames(bool value) => _settingsStore.lookupsZcashNames = value;
136 +
137 @action
138 void setLookupsWellKnown(bool value) => _settingsStore.lookupsWellKnown = value;
139
lib/view_model/settings/privacy_settings_view_model.dart
+3
@@ -99,6 +99,9 @@ abstract class PrivacySettingsViewModelBase with Store {
99 @computed
100 bool get looksUpENS => _settingsStore.lookupsENS;
101
102 + @computed
103 + bool get lookupsZcashNames => _settingsStore.lookupsZcashNames;
104 +
105 @computed
106 bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
107