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