Address resolver refactoring (#3291)
What changed, and why it matters
This is a large code refactor of Cake Wallet's address resolver feature, which turns human-readable names (like Twitter handles, ENS domains, or email-style addresses) into cryptocurrency addresses. The change reorganizes existing lookup logic into a cleaner provider/service pattern and adds new address sources such as Nostr, BIP353, and Zcash names. It is primarily a structural rewrite rather than an obvious security fix, but because it touches many parts of how the app decides where to send money, any bugs in the new resolver could lead to payments going to the wrong address.
Treat this as a high-risk refactor of payment-critical code. Review each AddressLookupProvider for correct input validation, DNS/HTTP response validation, and error handling. Ensure the first-match behavior in AddressResolverService cannot be abused by a malicious or compromised provider to redirect funds. Remove or fix the unreachable multi-currency branch. Add integration tests that verify each provider returns only valid, expected addresses for known handles and rejects malformed or attacker-controlled input. Consider requiring user confirmation when a resolved address differs from a previously saved contact address.
Security signals we found
Large refactor of address-resolution code that determines payment destinations
New providers perform network/DNS lookups based on user-typed handles and return addresses used in send flows
Address extraction from Twitter/Mastodon/Nostr bios relies on regex matching rather than cryptographic verification
Dead/unreachable code in AddressResolverService.resolve (return [] before multi-currency TODO block)
BIP353 provider fetches DNSSEC TXT records and silently accepts either a silent-payment or on-chain address
Zcash.me provider scrapes an HTML page with a regex for Zcash addresses
Thorchain provider accepts any non-empty string under 30 characters that is not already a normal address
No explicit security advisory, CVE, or researcher attribution present in the commit
Evidence from the diff
The commit refactors the legacy AddressResolver into a new AddressResolverService and a set of AddressLookupProvider implementations (ENS, FIO, OpenAlias, Yat, LNURL-pay, Mastodon, Twitter/X, Unstoppable Domains, WellKnown, Zano, Zcash, BIP353, Nostr, ThorChain). ParsedAddress is changed from a flat list of addresses to a per-currency map. The service iterates enabled providers, checks currency support and query patterns, and returns the first non-empty result. Most providers perform clearnet or DNS lookups via ProxyWrapper. Notable code-quality observations: the service has unreachable code after an early return (the multi-currency branch is dead code marked TODO), several providers accept arbitrary or weakly validated input, and address extraction from social-media bios uses regex patterns without strict validation. There is no evidence in the commit of a disclosed vulnerability or a targeted security patch.
Changed components
lib/core/address_resolver/address_resolver_service.dartlib/core/address_resolver/address_lookup_provider.dartlib/core/address_resolver/*_provider.dartlib/core/address_resolver/parsed_address.dartlib/core/address_validator.dartlib/di.dartlib/view_model/send/send_view_model.dartlib/src/screens/send/send_page.dartlib/src/screens/contact/contact_page.dartlib/new-ui/pages/send_page.dartlib/new-ui/pages/swap_page.dartInspect captured patch +3834 / −2586
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 5442dcff..8380897b 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 00000000..9feff561
--- /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<CryptoCurrency> get supportedCurrencies;
+
+ bool isEnabled(SettingsStore settingsStore);
+
+ bool canHandle(String query);
+
+ Future<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> 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 00000000..8592c53c
--- /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<AddressLookupProvider> _buildProviders() {
+ final allProviders = <AddressLookupProvider>[
+ 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<List<ParsedAddress>> 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 = <Future<List<ParsedAddress>>>[];
+
+ 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 00000000..f9f59240
--- /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<Map<CryptoCurrency, String>> resolveUnstoppableDomainFromText({
+ required String raw,
+ required List<CryptoCurrency> currencies,
+ }) async {
+ final domain = extractUnstoppableDomain(raw);
+ if (domain.isEmpty) return {};
+
+ final result = <CryptoCurrency, String>{};
+
+ 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<String>.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<CryptoCurrency, String> extractAddressesFromText({
+ required String raw,
+ required List<CryptoCurrency> currencies,
+ bool requireSurroundingWhitespaces = true,
+ }) {
+ final result = <CryptoCurrency, String>{};
+ 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'<br\s*/?>', caseSensitive: false), '\n')
+ .replaceAll(RegExp(r'</p\s*>', 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 00000000..729ef478
--- /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 00000000..3a46f87a
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> currencies,
+ required WalletBase wallet,
+ }) async {
+ try {
+ final result = <CryptoCurrency, String>{};
+ 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/core/address_resolver/bip_353/bip_353_record.dart b/lib/core/address_resolver/bip_353/bip_353_record.dart
new file mode 100644
index 00000000..257d7673
--- /dev/null
+++ b/lib/core/address_resolver/bip_353/bip_353_record.dart
@@ -0,0 +1,103 @@
+import 'dart:convert';
+import 'dart:isolate';
+
+import 'package:basic_utils/basic_utils.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:dnssec_proof/dnssec_proof.dart';
+
+class Bip353Record {
+ Bip353Record({
+ required this.uri,
+ required this.domain,
+ });
+
+ final String uri;
+ final String domain;
+
+ static const Map<String, String> keyDisplayMap = {
+ 'lno': 'BOLT 12 Offer',
+ 'sp': 'Silent Payment',
+ 'address': 'On-Chain Address',
+ };
+
+ static Future<String?> fetchDnsProof(String bip353Name) async {
+ if (bip353Name.startsWith('₿')) {
+ bip353Name = bip353Name.substring(1);
+ }
+ final parts = bip353Name.split('@');
+ if (parts.length != 2) return null;
+ final userPart = parts[0];
+ final domainPart = parts[1];
+ final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart.';
+ final proof = await Isolate.run(() => DnsProver.getTxtProof(bip353Domain));
+ return base64.encode(proof);
+ }
+
+ static Future<Map<String, String>?> fetchUriByCryptoCurrency(
+ String bip353Name, String asset) async {
+ try {
+ // 1. Parse the user and domain from "user@domain"
+ final parts = bip353Name.split('@');
+
+ if (parts.length != 2) return null;
+
+ String userPart = parts[0];
+ if (userPart.startsWith('₿')) {
+ userPart = userPart.substring(1);
+ }
+ final domainPart = parts[1];
+
+ // 2. Construct the correct subdomain: "user._bitcoin-payment.domain"
+ final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart';
+
+ // 3. Lookup the TXT record with DNSSEC
+ final txtRecords = await DnsUtils.lookupRecord(
+ bip353Domain,
+ RRecordType.TXT,
+ dnssec: true,
+ );
+ final proof = await fetchDnsProof(bip353Name);
+ if (proof == null) {
+ throw Exception('DNSSEC proof not found');
+ }
+
+ if (txtRecords == null) return null;
+
+ final assetName = CryptoCurrency.fromString(asset).fullName;
+ if (assetName == null) throw Exception('Invalid asset name');
+ final formattedAssetName = assetName.toLowerCase().replaceAll(' ', '') + ':';
+
+ for (final record in txtRecords) {
+ final data = record.data.replaceAll('"', '');
+ if (data.startsWith(formattedAssetName)) {
+ return _parseAssetUri(data, formattedAssetName);
+ }
+ }
+ } catch (e) {
+ printV('BIP353Record.fetchBitcoinUri error: $e');
+ }
+ return null;
+ }
+
+ static Map<String, String>? _parseAssetUri(String fullUri, String prefix) {
+ final afterPrefix = fullUri.substring(prefix.length);
+ if (afterPrefix.isEmpty) return null;
+
+ final questionIndex = afterPrefix.indexOf('?');
+ if (questionIndex == -1) {
+ return {'address': afterPrefix};
+ } else {
+ final addressPart = afterPrefix.substring(0, questionIndex);
+ final queryPart = afterPrefix.substring(questionIndex + 1);
+ final result = <String, String>{};
+
+ if (addressPart.isNotEmpty) result['address'] = addressPart;
+ final queryMap = Uri.splitQueryString(queryPart);
+ result.addAll(queryMap);
+
+ return result;
+ }
+ }
+
+}
\ 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 00000000..b0a365d0
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> currencies,
+ required WalletBase wallet,
+ }) async {
+ final Map<CryptoCurrency, String> 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 00000000..b976ae63
--- /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<String> 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 00000000..91d4ac72
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> currencies,
+ required WalletBase wallet,
+ }) async {
+ final Map<CryptoCurrency, String> 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<bool> checkAvail(String fioAddress) async {
+ bool isFioRegistered = false;
+ final headers = {'Content-Type': 'application/json'};
+ final body = <String, String>{"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<String, dynamic>;
+ isFioRegistered = responseJSON['is_registered'] as int == 1;
+
+ return isFioRegistered;
+ }
+
+ static Future<String?> getPubAddress(String fioAddress, String token) async {
+ final headers = {'Content-Type': 'application/json'};
+ final body = <String, String>{
+ "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<String, dynamic>;
+
+ 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 00000000..b767c15b
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> currencies,
+ required WalletBase wallet,
+ }) async {
+ try {
+ if (wallet.type != WalletType.bitcoin) return [];
+
+ final formattedName = query.trim();
+ if (formattedName.isEmpty) return [];
+
+ final result = <CryptoCurrency, String>{};
+
+ 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/core/address_resolver/lnurl_pay/lnurlpay_record.dart b/lib/core/address_resolver/lnurl_pay/lnurlpay_record.dart
new file mode 100644
index 00000000..35fec0d5
--- /dev/null
+++ b/lib/core/address_resolver/lnurl_pay/lnurlpay_record.dart
@@ -0,0 +1,76 @@
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/lnurl.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/utils/proxy_wrapper.dart';
+
+class LNUrlPayRecord {
+ LNUrlPayRecord({
+ required this.address,
+ required this.name,
+ });
+
+ final String name;
+ final String address;
+
+ static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency,
+ {bool allowRethrow = false}) async {
+ if (![CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return null;
+
+ // split the string by the @ symbol:
+ try {
+ final List<String> 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 = "_";
+ }
+
+ final expectedUrl = "https://$domain/.well-known/lnurlp/$name";
+ final response = await ProxyWrapper().get(
+ clearnetUri: Uri.parse(expectedUrl),
+ headers: <String, String>{"Accept": "application/json"},
+ );
+
+ if (response.statusCode == 200) {
+ return LNURL.encode(expectedUrl);
+ }
+ } catch (e) {
+ if (allowRethrow) rethrow;
+ 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<LNUrlPayRecord?> fetchAddressAndName({
+ required String formattedName,
+ required CryptoCurrency currency,
+ }) async {
+ final name = formattedName;
+
+ printV("formattedName: $formattedName");
+
+ final address = await checkWellKnownUsername(formattedName, currency);
+
+ if (address == null) {
+ return null;
+ }
+
+ return LNUrlPayRecord(address: address, name: name);
+ }
+}
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 00000000..10c226d0
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> 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 = <CryptoCurrency, String>{};
+
+ 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/core/address_resolver/mastodon/mastodon_api.dart b/lib/core/address_resolver/mastodon/mastodon_api.dart
new file mode 100644
index 00000000..6cb60be8
--- /dev/null
+++ b/lib/core/address_resolver/mastodon/mastodon_api.dart
@@ -0,0 +1,66 @@
+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';
+
+class MastodonAPI {
+ static const httpsScheme = 'https';
+ static const userPath = '/api/v1/accounts/lookup';
+ static const statusesPath = '/api/v1/accounts/:id/statuses';
+
+ static Future<MastodonUser?> lookupUserByUserName(
+ {required String userName, required String apiHost}) async {
+ try {
+ final queryParams = {'acct': userName};
+
+ final uri = Uri(
+ scheme: httpsScheme,
+ host: apiHost,
+ path: userPath,
+ queryParameters: queryParams,
+ );
+
+ final response = await ProxyWrapper().get(clearnetUri: uri);
+
+
+ if (response.statusCode != 200) return null;
+
+ final Map<String, dynamic> responseJSON = json.decode(response.body) as Map<String, dynamic>;
+
+ return MastodonUser.fromJson(responseJSON);
+ } catch (e) {
+ printV('Error in lookupUserByUserName: $e');
+ return null;
+ }
+ }
+
+ static Future<List<PinnedPost>> getPinnedPosts({
+ required String userId,
+ required String apiHost,
+ }) async {
+ try {
+ final queryParams = {'pinned': 'true'};
+
+ final uri = Uri(
+ scheme: httpsScheme,
+ host: apiHost,
+ path: statusesPath.replaceAll(':id', userId),
+ queryParameters: queryParams,
+ );
+
+ final response = await ProxyWrapper().get(clearnetUri: uri);
+
+
+ if (response.statusCode != 200) {
+ throw Exception('Unexpected HTTP status: ${response.statusCode}');
+ }
+
+ final List<dynamic> responseJSON = json.decode(response.body) as List<dynamic>;
+
+ return responseJSON.map((json) => PinnedPost.fromJson(json as Map<String, dynamic>)).toList();
+ } catch (e) {
+ printV('Error in getPinnedPosts: $e');
+ throw e;
+ }
+ }
+}
diff --git a/lib/core/address_resolver/mastodon/mastodon_user.dart b/lib/core/address_resolver/mastodon/mastodon_user.dart
new file mode 100644
index 00000000..1832c083
--- /dev/null
+++ b/lib/core/address_resolver/mastodon/mastodon_user.dart
@@ -0,0 +1,39 @@
+class MastodonUser {
+ String id;
+ String username;
+ String profileImageUrl;
+ String acct;
+ String note;
+
+ MastodonUser({
+ required this.id,
+ required this.username,
+ required this.profileImageUrl,
+ required this.acct,
+ required this.note,
+ });
+
+ factory MastodonUser.fromJson(Map<String, dynamic> json) {
+ return MastodonUser(
+ id: json['id'] as String,
+ username: json['username'] as String? ?? '',
+ acct: json['acct'] as String,
+ note: json['note'] as String,
+ profileImageUrl: json['avatar'] as String? ?? ''
+ );
+ }
+}
+
+class PinnedPost {
+ final String id;
+ final String content;
+
+ PinnedPost({required this.id, required this.content});
+
+ factory PinnedPost.fromJson(Map<String, dynamic> json) {
+ return PinnedPost(
+ id: json['id'] as String,
+ content: json['content'] as String,
+ );
+ }
+}
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 00000000..332187e9
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> 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 = <CryptoCurrency, String>{};
+
+ 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 00000000..19d25c01
--- /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<ProfilePointer?> queryProfile(String nip05Address) async {
+ final profile = await _nip05.queryProfile(nip05Address);
+ if (profile?.pubkey != null && profile?.relays?.isNotEmpty == true) {
+ return profile;
+ }
+ return null;
+ }
+
+ static Future<UserMetadata?> 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<UserMetadata?> _fetchInfoFromRelay(
+ String relayUrl, String userPubKey, List<int> kinds) async {
+ // sanitize so obvious junk (like '#') doesn't reach connect()
+ final clean = _sanitizeRelay(relayUrl);
+ if (clean.isEmpty) return null;
+
+ final result = Completer<UserMetadata?>();
+
+ 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<String, dynamic>;
+ 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/core/address_resolver/nostr/nostr_user.dart b/lib/core/address_resolver/nostr/nostr_user.dart
new file mode 100644
index 00000000..4b1694a1
--- /dev/null
+++ b/lib/core/address_resolver/nostr/nostr_user.dart
@@ -0,0 +1,35 @@
+
+class UserMetadata {
+ final String name;
+ final String lnurl;
+ final String email;
+ final String picture;
+ final String about;
+ final String nip05;
+ final String banner;
+ final String website;
+
+ UserMetadata({
+ required this.name,
+ required this.lnurl,
+ required this.email,
+ required this.picture,
+ required this.about,
+ required this.nip05,
+ required this.banner,
+ required this.website,
+ });
+
+ factory UserMetadata.fromJson(Map<String, dynamic> json) {
+ return UserMetadata(
+ name: json['name'] as String? ?? '',
+ lnurl: json['lud06'] as String? ?? '',
+ email: json['lud16'] as String? ?? '',
+ picture: json['picture'] as String? ?? '',
+ about: json['about'] as String? ?? '',
+ nip05: json['nip05'] as String? ?? '',
+ banner: json['banner'] as String? ?? '',
+ website: json['website'] as String? ?? '',
+ );
+ }
+}
\ No newline at end of file
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 00000000..6f7e8671
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> currencies,
+ required WalletBase wallet,
+ }) async {
+ try {
+ final formatted = OpenaliasRecord.formatDomainName(query);
+
+ final txtRecords = await OpenaliasRecord.lookupOpenAliasRecord(formatted);
+ if (txtRecords == null) return [];
+
+ final result = <CryptoCurrency, String>{};
+
+ 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 00000000..65273df3
--- /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<List<RRecord>?> 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<RRecord> 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 00000000..8934e2d7
--- /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<CryptoCurrency, String> 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 00000000..21c27217
--- /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<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
+
+ @override
+ bool canHandle(String q) => q.isNotEmpty; // can be any string
+
+ @override
+ bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsThorChain;
+
+ @override
+ Future<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> 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 = <CryptoCurrency, String>{};
+
+ 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 00000000..954bfb6e
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> currencies,
+ required WalletBase wallet,
+ }) async {
+ final formattedName = query.replaceFirst("@", "");
+ try {
+ final result = <CryptoCurrency, String>{};
+
+ 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<CryptoCurrency, String> result,
+ ) {
+ return ParsedAddress(
+ parsedAddressByCurrencyMap: result,
+ addressSource: AddressSource.twitter,
+ handle: query,
+ profileImageUrl: twitterUser.profileImageUrl,
+ profileName: twitterUser.name,
+ );
+ }
+}
diff --git a/lib/core/address_resolver/twitter/twitter_api.dart b/lib/core/address_resolver/twitter/twitter_api.dart
new file mode 100644
index 00000000..0079d39a
--- /dev/null
+++ b/lib/core/address_resolver/twitter/twitter_api.dart
@@ -0,0 +1,67 @@
+import 'dart:convert';
+
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
+import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart';
+import 'package:cw_core/utils/proxy_wrapper.dart';
+
+class TwitterApi {
+ static const twitterBearerToken = secrets.twitterBearerToken;
+ static const httpsScheme = 'https';
+ static const apiHost = 'api.twitter.com';
+ static const userPath = '/2/users/by/username/';
+
+ static Future<TwitterUser> lookupUserByName({required String userName}) async {
+ final queryParams = {
+ 'user.fields': 'description,profile_image_url,location',
+ 'expansions': 'pinned_tweet_id',
+ 'tweet.fields': 'note_tweet'
+ };
+ final headers = {'authorization': 'Bearer $twitterBearerToken'};
+ final uri = Uri(
+ scheme: httpsScheme,
+ host: apiHost,
+ path: userPath + userName,
+ queryParameters: queryParams);
+
+ final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers).catchError((error) {
+ throw Exception('HTTP request failed: $error');
+ });
+
+ if (response.statusCode != 200) {
+ throw Exception('Unexpected http status: ${response.statusCode}');
+ }
+
+
+ final Map<String, dynamic> responseJSON = jsonDecode(response.body) as Map<String, dynamic>;
+ if (responseJSON['errors'] != null &&
+ !responseJSON['errors'][0]['detail']
+ .toString()
+ .contains("Could not find tweet with pinned_tweet_id")) {
+ throw Exception(responseJSON['errors'][0]['detail']);
+ }
+
+ return TwitterUser.fromJson(responseJSON, _getPinnedTweet(responseJSON));
+ }
+
+ static Tweet? _getPinnedTweet(Map<String, dynamic> responseJSON) {
+ try {
+ final tweetId = responseJSON['data']['pinned_tweet_id'] as String?;
+ if (tweetId == null || responseJSON['includes'] == null) return null;
+
+ final tweetIncludes = List.from(responseJSON['includes']['tweets'] as List);
+ final pinnedTweetData = tweetIncludes.firstWhere(
+ (tweet) => tweet['id'] == tweetId,
+ orElse: () => null,
+ ) as Map<String, dynamic>?;
+
+ if (pinnedTweetData == null) return null;
+
+ final pinnedTweetText =
+ (pinnedTweetData['note_tweet']?['text'] ?? pinnedTweetData['text']) as String;
+
+ return Tweet(id: tweetId, text: pinnedTweetText);
+ } catch (e) {
+ return null;
+ }
+ }
+}
diff --git a/lib/core/address_resolver/twitter/twitter_user.dart b/lib/core/address_resolver/twitter/twitter_user.dart
new file mode 100644
index 00000000..f124de40
--- /dev/null
+++ b/lib/core/address_resolver/twitter/twitter_user.dart
@@ -0,0 +1,49 @@
+class TwitterUser {
+ TwitterUser(
+ {required this.id,
+ required this.username,
+ required this.name,
+ required this.description,
+ required this.location,
+ required this.profileImageUrl,
+ this.pinnedTweet});
+
+ final String id;
+ final String username;
+ final String name;
+ final String description;
+ final String location;
+ final String profileImageUrl;
+ final Tweet? pinnedTweet;
+
+ factory TwitterUser.fromJson(Map<String, dynamic> json, [Tweet? pinnedTweet]) {
+ final profileImageUrl = json['data']['profile_image_url'] as String? ?? '';
+ final scaledProfileImageUrl = profileImageUrl.replaceFirst('normal', '200x200');
+ return TwitterUser(
+ id: json['data']['id'] as String,
+ username: json['data']['username'] as String? ?? '',
+ name: json['data']['name'] as String,
+ description: json['data']['description'] as String? ?? '',
+ location: json['data']['location'] as String? ?? '',
+ profileImageUrl: scaledProfileImageUrl,
+ pinnedTweet: pinnedTweet,
+ );
+ }
+}
+
+class Tweet {
+ Tweet({
+ required this.id,
+ required this.text,
+ });
+
+ final String id;
+ final String text;
+
+ factory Tweet.fromJson(Map<String, dynamic> json) {
+ return Tweet(
+ id: json['id'] as String,
+ text: json['text'] as String,
+ );
+ }
+}
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 00000000..dd240f7e
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> currencies,
+ required WalletBase wallet,
+ }) async {
+ try {
+ final formattedName = OpenaliasRecord.formatDomainName(query);
+ final result = <CryptoCurrency, String>{};
+
+ 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<String> 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<String, dynamic>;
+ if (jsonParsed["records"] == null) {
+ throw Exception(".records response from $uri is empty");
+ };
+ final records = jsonParsed["records"] as Map<String, dynamic>;
+ 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 00000000..464ffd79
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> 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 00000000..b2ed2eb9
--- /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<WellKnownRecord?> 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<String, dynamic>;
+ final items = body['names'] as List<dynamic>? ?? const [];
+
+ for (final raw in items.whereType<Map>()) {
+ 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 00000000..d8ee3454
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> currencies,
+ required WalletBase wallet,
+ }) async {
+ try {
+ final result = <CryptoCurrency, String>{};
+
+ 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 00000000..412a5a48
--- /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<String, dynamic> 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 00000000..84ba4e8f
--- /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<List<YatRecord>> 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<String, dynamic>;
+
+ final res = body['result'];
+ if (res == null) return const [];
+
+ final records = <YatRecord>[];
+
+ if (res is Map) {
+ res.forEach((tag, data) {
+ if (data is Map<String, dynamic>) {
+ records.add(YatRecord.fromJson(data, tag.toString()));
+ }
+ });
+ }
+
+ else if (res is List) {
+ for (final item in res) {
+ if (item is Map<String, dynamic>) {
+ 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/core/address_resolver/yat/yat_store.dart b/lib/core/address_resolver/yat/yat_store.dart
new file mode 100644
index 00000000..860eecee
--- /dev/null
+++ b/lib/core/address_resolver/yat/yat_store.dart
@@ -0,0 +1,284 @@
+import 'package:cake_wallet/core/secure_storage.dart';
+import 'package:cw_core/transaction_history.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/wallet_base.dart';
+import 'package:cw_core/balance.dart';
+import 'package:cw_core/transaction_info.dart';
+import 'package:cake_wallet/store/app_store.dart';
+import 'package:mobx/mobx.dart';
+import 'package:cw_core/wallet_type.dart';
+import 'dart:async';
+
+part 'yat_store.g.dart';
+
+class YatLink {
+ static const partnerId = ''; // 'CW';
+ static const baseDevUrl = ''; // 'https://yat.fyi';
+ static const baseReleaseUrl = ''; // 'https://y.at';
+ static const signInSuffix = ''; // '/partner/$partnerId/link-email';
+ static const createSuffix = ''; // '/create';
+ static const managePath = ''; // '/partner/$partnerId/manage';
+ static const queryParameter = ''; // '?address_json=';
+ static const apiDevUrl = ''; // 'https://a.yat.fyi';
+ static const apiReleaseUrl = ''; // 'https://a.y.at';
+ static const requestDevUrl = ''; // 'https://a.yat.fyi/emoji_id/';
+ static const requestReleaseUrl = ''; //'https://a.y.at/emoji_id/';
+ static const startFlowUrl = ''; // 'https://www.y03btrk.com/4RQSJ/6JHXF/';
+ static const isDevMode = true;
+ static const tags = <String, List<String>>{"XMR" : ['0x1001', '0x1002'],
+ "BTC" : ['0x1003'], "LTC" : ['0x1019']};
+
+ static String get apiUrl => YatLink.isDevMode
+ ? YatLink.apiDevUrl
+ : YatLink.apiReleaseUrl;
+
+ static String get emojiIdUrl => apiUrl + '/emoji_id/';
+
+ static String get baseUrl => YatLink.isDevMode
+ ? YatLink.baseDevUrl
+ : YatLink.baseReleaseUrl;
+}
+
+Future<List<String>> fetchYatAddress(String emojiId, String ticker) async {
+ throw Exception();
+ //final url = YatLink.emojiIdUrl + emojiId + '/payment';
+ //final response = await get(url);
+
+ //if (response.statusCode != 200) {
+ // throw YatException(text: response.body.toString());
+ //}
+
+ //final addresses = <String>[];
+ //final currency = ticker.toUpperCase();
+ //final responseJSON = json.decode(response.body) as Map<String, dynamic>;
+ //final result = responseJSON['result'] as Map<dynamic, dynamic>;
+ //result.forEach((dynamic key, dynamic value) {
+ // final tag = key as String ?? '';
+ // final record = value as Map<String, dynamic>;
+
+ // if (YatLink.tags[currency]?.contains(tag) ?? false) {
+ // final address = record['address'] as String;
+ // if (address?.isNotEmpty ?? false) {
+ // addresses.add(address);
+ // }
+ // }
+ //});
+
+ //return addresses;
+ return [];
+}
+
+Future<String> fetchYatAccessToken(String refreshToken) async {
+ throw Exception();
+ //try {
+ // final url = YatLink.apiUrl + '/auth/token/refresh';
+ // final bodyJson = json.encode({'refresh_token': refreshToken});
+ // final response = await post(
+ // url,
+ // headers: <String, String>{
+ // 'Content-Type': 'application/json',
+ // 'Accept': '*/*'
+ // },
+ // body: bodyJson);
+
+ // if (response.statusCode != 200) {
+ // throw YatException(text: response.body.toString());
+ // }
+
+ // final responseJSON = json.decode(response.body) as Map<String, dynamic>;
+ // return responseJSON['access_token'] as String;
+ //}catch(_) {
+ // return '';
+ //}
+ return '';
+}
+
+Future<String> fetchYatApiKey(String accessKey) async {
+ throw Exception();
+ //try {
+ // final url = YatLink.apiUrl + '/api_keys';
+ // final bodyJson = json.encode({'name': 'CW'});
+ // final response = await post(
+ // url,
+ // headers: <String, String>{
+ // 'Authorization': 'Bearer $accessKey',
+ // 'Content-Type': 'application/json',
+ // 'Accept': '*/*'
+ // },
+ // body: bodyJson);
+
+ // if (response.statusCode != 200) {
+ // throw YatException(text: response.body.toString());
+ // }
+
+ // final responseJSON = json.decode(response.body) as Map<String, dynamic>;
+ // return responseJSON['api_key'] as String;
+ //}catch(_) {
+ // return '';
+ //}
+ return '';
+}
+
+Future<void> updateEmojiIdAddress(String emojiId, String address, String apiKey, WalletType type) async {
+ throw Exception();
+ //final url = YatLink.emojiIdUrl + emojiId;
+ //final cur = walletTypeToCryptoCurrency(type);
+ //final curFormatted = cur.toString().toUpperCase();
+ //var tag = '';
+
+ //if (type == WalletType.monero && !address.startsWith('4')) {
+ // tag = YatLink.tags[curFormatted].last;
+ //} else {
+ // tag = YatLink.tags[curFormatted].first;
+ //}
+
+ //final bodyJson = json.encode({
+ // 'insert': [{
+ // 'data': address,
+ // 'tag': tag
+ // }]
+ //});
+ //final response = await patch(
+ // url,
+ // headers: <String, String>{
+ // 'x-api-key': apiKey,
+ // 'Content-Type': 'application/json',
+ // 'Accept': '*/*'
+ // },
+ // body: bodyJson);
+
+ //if (response.statusCode != 200) {
+ // throw YatException(text: response.body.toString());
+ //}
+}
+
+Future<String> visualisationForEmojiId(String emojiId) async {
+ throw Exception();
+ //final url = YatLink.emojiIdUrl + emojiId + '/json/VisualizerFileLocations';
+ //final response = await get(url);
+ //final responseJSON = json.decode(response.body) as Map<String, dynamic>;
+ //final data = responseJSON['data'] as Map<String, dynamic>;
+ //final result = data['gif'] as String ?? '';
+ //return result;
+ return '';
+}
+
+class YatStore = YatStoreBase with _$YatStore;
+
+abstract class YatStoreBase with Store {
+ YatStoreBase({
+ required this.appStore,
+ required this.secureStorage})
+ : _wallet = appStore.wallet,
+ emoji = appStore.wallet?.walletInfo?.yatEmojiId ?? '',
+ refreshToken = '',
+ accessToken = '',
+ apiKey = '',
+ emojiIncommingSC = StreamController<String>.broadcast() {
+ //reaction((_) => appStore.wallet, _onWalletChange);
+ //reaction((_) => emoji, (String _) => _onEmojiChange());
+ //reaction((_) => refreshToken, (String _) => _onRefreshTokenChange());
+ }
+
+ static const yatRefreshTokenKeyBase = 'yat_refresh_token';
+ static const yatAccessTokenKeyBase = 'yat_access_token';
+ static const yatApiKeyBase = 'yat_api_key';
+
+ static String yatRefreshTokenKey(String name) => '${yatRefreshTokenKeyBase}_$name';
+ static String yatAccessTokenKey(String name) => '${yatAccessTokenKeyBase}_$name';
+ static String yatApiKey(String name) => '${yatApiKeyBase}_$name';
+
+ AppStore appStore;
+
+ SecureStorage secureStorage;
+
+ @observable
+ String emoji;
+
+ @observable
+ String refreshToken;
+
+ @observable
+ String accessToken;
+
+ @observable
+ String apiKey;
+
+ StreamController<String> emojiIncommingSC;
+
+ Stream<String> get emojiIncommingStream => emojiIncommingSC.stream;
+
+ @observable
+ WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
+ _wallet;
+
+ Future<void> init() async {
+ if (_wallet == null) {
+ return;
+ }
+
+ refreshToken = await secureStorage.read(key: yatRefreshTokenKey(_wallet!.walletInfo.name)) ?? '';
+ accessToken = await secureStorage.read(key: yatAccessTokenKey(_wallet!.walletInfo.name)) ?? '';
+ apiKey = await secureStorage.read(key: yatApiKey(_wallet!.walletInfo.name)) ?? '';
+ }
+
+ @action
+ void _onWalletChange(
+ WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
+ TransactionInfo>
+ wallet) {
+ this._wallet = wallet;
+ emoji = wallet?.walletInfo?.yatEmojiId ?? '';
+ init();
+ }
+
+ @action
+ void _onEmojiChange() {
+ try {
+ final walletInfo = _wallet?.walletInfo;
+
+ if (walletInfo == null) {
+ return;
+ }
+
+ walletInfo!.yatEid = emoji;
+
+ walletInfo!.save();
+ } catch (e) {
+ printV(e.toString());
+ }
+ }
+
+ @action
+ Future<void> _onRefreshTokenChange() async {
+ throw Exception();
+
+ //try {
+ // await secureStorage.write(key: yatRefreshTokenKey(_wallet.walletInfo.name), value: refreshToken);
+ // accessToken = await fetchYatAccessToken(refreshToken);
+ // await secureStorage.write(key: yatAccessTokenKey(_wallet.walletInfo.name), value: accessToken);
+ // apiKey = await fetchYatApiKey(accessToken);
+ // await secureStorage.write(key: yatApiKey(_wallet.walletInfo.name), value: accessToken);
+ //} catch (e) {
+ // printV(e.toString());
+ //}
+ }
+
+ String defineQueryParameters() {
+ throw Exception();
+ //final result = <String, String>{};
+ //final tags = YatLink.tags[_wallet.currency.toString().toUpperCase()];
+ //String tag = tags.first;
+
+ //if (_wallet.type == WalletType.monero
+ // && _wallet.walletAddresses.address.startsWith('4')) {
+ // tag = tags.last;
+ //}
+ //result[tag] = '${_wallet.walletAddresses.address}|${_wallet.name}';
+ //final addressJson = json.encode([result]);
+ //final addressJsonBytes = utf8.encode(addressJson);
+
+ //return base64.encode(addressJsonBytes);
+ return '';
+ }
+}
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 00000000..e0285640
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> 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<String?> 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<String, dynamic>;
+ 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 00000000..5da25fdd
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> 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 00000000..6dd0968d
--- /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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
+ required String query,
+ required List<CryptoCurrency> 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<String?> _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/core/address_resolver/zcash/zcash_names_record.dart b/lib/core/address_resolver/zcash/zcash_names_record.dart
new file mode 100644
index 00000000..1a20a472
--- /dev/null
+++ b/lib/core/address_resolver/zcash/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<String?> 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<String, dynamic>) return null;
+ if (decoded['error'] != null) return null;
+
+ final result = decoded['result'];
+ if (result is! Map<String, dynamic>) 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/core/address_validator.dart b/lib/core/address_validator.dart
index 0d44bad6..d6934542 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<CryptoCurrency> 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 84aac162..00000000
--- 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<List<YatRecord>> 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: <String, dynamic>{
- "tags": tag
- });
- final yatRecords = <YatRecord>[];
-
- try {
- final response = await ProxyWrapper().get(clearnetUri: uri);
-
- final resBody = json.decode(response.body) as Map<String, dynamic>;
- final results = resBody["result"] as Map<dynamic, dynamic>;
- // Favour a subaddress over a standard address.
- final yatRecord = (
- results[MONERO_SUB_ADDRESS] ??
- results[MONERO_STD_ADDRESS] ??
- results[tag]) as Map<String, dynamic>;
-
- if (yatRecord.isNotEmpty) {
- yatRecords.add(YatRecord.fromJson(yatRecord));
- }
-
- return yatRecords;
- } catch (_) {
- return yatRecords;
- }
- }
-}
diff --git a/lib/di.dart b/lib/di.dart
index 821f5113..bf2b6444 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<void> setup({
getIt.get<AppStore>(),
getIt.get<SendTemplateViewModel>(),
getIt.get<FiatConversionStore>(),
+ getIt.get<AddressResolverService>(),
getIt.get<BalanceViewModel>(),
getIt.get<ContactListViewModel>(),
_transactionDescriptionBox,
@@ -1100,7 +1101,10 @@ Future<void> setup({
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
- (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
+ (ContactRecord? contact, _) => ContactViewModel(_contactSource,
+ getIt.get<AppStore>(),
+ getIt.get<AddressResolverService>(),
+ contact: contact));
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
(CryptoCurrency? cur, _) =>
@@ -1249,7 +1253,10 @@ Future<void> setup({
getIt.registerFactoryParam<ExchangePage, PaymentRequest?, void>(
(PaymentRequest? paymentRequest, __) {
- return ExchangePage(getIt.get<ExchangeViewModel>(), getIt.get<AuthService>(), paymentRequest);
+ return ExchangePage(getIt.get<ExchangeViewModel>(),
+ getIt.get<AuthService>(),
+ getIt.get<AddressResolverService>(),
+ paymentRequest);
});
getIt.registerFactoryParam<NewSwapPage, PaymentRequest?, CryptoCurrency?>(
@@ -1257,6 +1264,7 @@ Future<void> setup({
return NewSwapPage(
getIt.get<ExchangeViewModel>(),
getIt.get<AuthService>(),
+ getIt.get<AddressResolverService>(),
paymentRequest,
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
initialCurrency: initialCurrency,
@@ -1494,7 +1502,7 @@ Future<void> setup({
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
- getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
+ getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>(), getIt.get<AddressResolverService>()));
getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableItem>;
@@ -1592,10 +1600,9 @@ Future<void> setup({
getIt.registerFactory(() => YatService());
- getIt.registerFactory(() => AddressResolver(
- yatService: getIt.get<YatService>(),
- wallet: getIt.get<AppStore>().wallet!,
- settingsStore: getIt.get<SettingsStore>()));
+ getIt.registerFactory<AddressResolverService>(
+ () => AddressResolverService(settingsStore: getIt.get<SettingsStore>()),
+ );
getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>(
(QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData));
diff --git a/lib/entities/bip_353_record.dart b/lib/entities/bip_353_record.dart
deleted file mode 100644
index 9a2a50e0..00000000
--- a/lib/entities/bip_353_record.dart
+++ /dev/null
@@ -1,172 +0,0 @@
-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({
- required this.uri,
- required this.domain,
- });
-
- final String uri;
- final String domain;
-
- static const Map<String, String> keyDisplayMap = {
- 'lno': 'BOLT 12 Offer',
- 'sp': 'Silent Payment',
- 'address': 'On-Chain Address',
- };
-
- static Future<String?> fetchDnsProof(String bip353Name) async {
- if (bip353Name.startsWith('₿')) {
- bip353Name = bip353Name.substring(1);
- }
- final parts = bip353Name.split('@');
- if (parts.length != 2) return null;
- final userPart = parts[0];
- final domainPart = parts[1];
- final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart.';
- final proof = await Isolate.run(() => DnsProver.getTxtProof(bip353Domain));
- return base64.encode(proof);
- }
-
- static Future<Map<String, String>?> fetchUriByCryptoCurrency(
- String bip353Name, String asset) async {
- try {
- // 1. Parse the user and domain from "user@domain"
- final parts = bip353Name.split('@');
-
- if (parts.length != 2) return null;
-
- String userPart = parts[0];
- if (userPart.startsWith('₿')) {
- userPart = userPart.substring(1);
- }
- final domainPart = parts[1];
-
- // 2. Construct the correct subdomain: "user._bitcoin-payment.domain"
- final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart';
-
- // 3. Lookup the TXT record with DNSSEC
- final txtRecords = await DnsUtils.lookupRecord(
- bip353Domain,
- RRecordType.TXT,
- dnssec: true,
- );
- final proof = await fetchDnsProof(bip353Name);
- if (proof == null) {
- throw Exception('DNSSEC proof not found');
- }
-
- if (txtRecords == null) return null;
-
- final assetName = CryptoCurrency.fromString(asset).fullName;
- if (assetName == null) throw Exception('Invalid asset name');
- final formattedAssetName = assetName.toLowerCase().replaceAll(' ', '') + ':';
-
- for (final record in txtRecords) {
- final data = record.data.replaceAll('"', '');
- if (data.startsWith(formattedAssetName)) {
- return _parseAssetUri(data, formattedAssetName);
- }
- }
- } catch (e) {
- printV('BIP353Record.fetchBitcoinUri error: $e');
- }
- return null;
- }
-
- static Map<String, String>? _parseAssetUri(String fullUri, String prefix) {
- final afterPrefix = fullUri.substring(prefix.length);
- if (afterPrefix.isEmpty) return null;
-
- final questionIndex = afterPrefix.indexOf('?');
- if (questionIndex == -1) {
- return {'address': afterPrefix};
- } else {
- final addressPart = afterPrefix.substring(0, questionIndex);
- final queryPart = afterPrefix.substring(questionIndex + 1);
- final result = <String, String>{};
-
- if (addressPart.isNotEmpty) result['address'] = addressPart;
- final queryMap = Uri.splitQueryString(queryPart);
- result.addAll(queryMap);
-
- return result;
- }
- }
-
- static Future<String?> pickBip353AddressChoice(
- BuildContext context,
- String bip353Name,
- Map<String, String> addressMap,
- ) async {
- if (addressMap.length == 1) {
- return addressMap.values.first;
- }
-
- final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap);
-
- return chosenAddress;
- }
-
- static Future<String?> _showAddressChoiceDialog(
- BuildContext context,
- String bip353Name,
- Map<String, String> addressMap,
- ) async {
- final entriesList = addressMap.entries.toList();
- final List<Map<String, String>> 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<void>(
- 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<String, String> 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';
- }
-
-}
diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart
deleted file mode 100644
index 239b5f45..00000000
--- 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<String> 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 dcfd79c9..00000000
--- 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<bool> checkAvail(String fioAddress) async {
- bool isFioRegistered = false;
- final headers = {'Content-Type': 'application/json'};
- final body = <String, String>{"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<String, dynamic>;
- isFioRegistered = responseJSON['is_registered'] as int == 1;
-
- return isFioRegistered;
- }
-
- static Future<String> getPubAddress(String fioAddress, String token) async {
- final headers = {'Content-Type': 'application/json'};
- final body = <String, String>{
- "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<String, dynamic>;
- 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<String, dynamic>;
- final String pubAddress = responseJSON['public_address'] as String;
-
- return pubAddress;
- }
-}
diff --git a/lib/entities/lnurlpay_record.dart b/lib/entities/lnurlpay_record.dart
deleted file mode 100644
index 35fec0d5..00000000
--- a/lib/entities/lnurlpay_record.dart
+++ /dev/null
@@ -1,76 +0,0 @@
-import 'package:cw_core/crypto_currency.dart';
-import 'package:cw_core/lnurl.dart';
-import 'package:cw_core/utils/print_verbose.dart';
-import 'package:cw_core/utils/proxy_wrapper.dart';
-
-class LNUrlPayRecord {
- LNUrlPayRecord({
- required this.address,
- required this.name,
- });
-
- final String name;
- final String address;
-
- static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency,
- {bool allowRethrow = false}) async {
- if (![CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return null;
-
- // split the string by the @ symbol:
- try {
- final List<String> 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 = "_";
- }
-
- final expectedUrl = "https://$domain/.well-known/lnurlp/$name";
- final response = await ProxyWrapper().get(
- clearnetUri: Uri.parse(expectedUrl),
- headers: <String, String>{"Accept": "application/json"},
- );
-
- if (response.statusCode == 200) {
- return LNURL.encode(expectedUrl);
- }
- } catch (e) {
- if (allowRethrow) rethrow;
- 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<LNUrlPayRecord?> fetchAddressAndName({
- required String formattedName,
- required CryptoCurrency currency,
- }) async {
- final name = formattedName;
-
- printV("formattedName: $formattedName");
-
- final address = await checkWellKnownUsername(formattedName, currency);
-
- if (address == null) {
- return null;
- }
-
- return LNUrlPayRecord(address: address, name: name);
- }
-}
diff --git a/lib/entities/openalias_record.dart b/lib/entities/openalias_record.dart
deleted file mode 100644
index b2af2e53..00000000
--- 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<List<RRecord>?> 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<RRecord> 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 7a6a47bc..00000000
--- 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<String>.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'<br\s*/?>', caseSensitive: false), '\n')
- .replaceAll(RegExp(r'</p\s*>', caseSensitive: false), '\n')
- .replaceAll(RegExp(r'<[^>]*>'), '')
- .trim();
- }
-
- Future<ParsedAddress> 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<String?> _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 28bdf704..00000000
--- 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<YatRecord>? 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<String> 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 be90ed6a..c052633a 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 cf6eb894..00000000
--- 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<String> 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<String, dynamic>;
- if (jsonParsed["records"] == null) {
- throw Exception(".records response from $uri is empty");
- };
- final records = jsonParsed["records"] as Map<String, dynamic>;
- 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 6dd94440..00000000
--- 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<String?> 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<String> 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: <String, String>{"Accept": "application/json"},
- );
-
- if (response.statusCode != 200) {
- return null;
- }
-
- final Map<String, dynamic> decoded = json.decode(response.body) as Map<String, dynamic>;
-
- // Access the first element in the names array and retrieve its address
- final List<dynamic> names = decoded["names"] as List<dynamic>;
- 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<WellKnownRecord?> 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 40e677a8..00000000
--- a/lib/entities/yat_record.dart
+++ /dev/null
@@ -1,13 +0,0 @@
-class YatRecord {
- YatRecord({
- required this.category,
- required this.address,
- });
-
- YatRecord.fromJson(Map<String, dynamic> 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 3d1c7496..00000000
--- 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<String?> 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<String, dynamic>;
-
- return jsonParsed['result']['alias_details']['address'] as String?;
- } catch (e) {
- printV('Zano Alias error: ${e.toString()}');
- }
-
- return null;
- }
-}
diff --git a/lib/entities/zcash_names_record.dart b/lib/entities/zcash_names_record.dart
deleted file mode 100644
index 1a20a472..00000000
--- a/lib/entities/zcash_names_record.dart
+++ /dev/null
@@ -1,75 +0,0 @@
-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<String?> 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<String, dynamic>) return null;
- if (decoded['error'] != null) return null;
-
- final result = decoded['result'];
- if (result is! Map<String, dynamic>) 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/mastodon/mastodon_api.dart b/lib/mastodon/mastodon_api.dart
deleted file mode 100644
index 58767eea..00000000
--- a/lib/mastodon/mastodon_api.dart
+++ /dev/null
@@ -1,66 +0,0 @@
-import 'dart:convert';
-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';
- static const userPath = '/api/v1/accounts/lookup';
- static const statusesPath = '/api/v1/accounts/:id/statuses';
-
- static Future<MastodonUser?> lookupUserByUserName(
- {required String userName, required String apiHost}) async {
- try {
- final queryParams = {'acct': userName};
-
- final uri = Uri(
- scheme: httpsScheme,
- host: apiHost,
- path: userPath,
- queryParameters: queryParams,
- );
-
- final response = await ProxyWrapper().get(clearnetUri: uri);
-
-
- if (response.statusCode != 200) return null;
-
- final Map<String, dynamic> responseJSON = json.decode(response.body) as Map<String, dynamic>;
-
- return MastodonUser.fromJson(responseJSON);
- } catch (e) {
- printV('Error in lookupUserByUserName: $e');
- return null;
- }
- }
-
- static Future<List<PinnedPost>> getPinnedPosts({
- required String userId,
- required String apiHost,
- }) async {
- try {
- final queryParams = {'pinned': 'true'};
-
- final uri = Uri(
- scheme: httpsScheme,
- host: apiHost,
- path: statusesPath.replaceAll(':id', userId),
- queryParameters: queryParams,
- );
-
- final response = await ProxyWrapper().get(clearnetUri: uri);
-
-
- if (response.statusCode != 200) {
- throw Exception('Unexpected HTTP status: ${response.statusCode}');
- }
-
- final List<dynamic> responseJSON = json.decode(response.body) as List<dynamic>;
-
- return responseJSON.map((json) => PinnedPost.fromJson(json as Map<String, dynamic>)).toList();
- } catch (e) {
- printV('Error in getPinnedPosts: $e');
- throw e;
- }
- }
-}
diff --git a/lib/mastodon/mastodon_user.dart b/lib/mastodon/mastodon_user.dart
deleted file mode 100644
index 1832c083..00000000
--- a/lib/mastodon/mastodon_user.dart
+++ /dev/null
@@ -1,39 +0,0 @@
-class MastodonUser {
- String id;
- String username;
- String profileImageUrl;
- String acct;
- String note;
-
- MastodonUser({
- required this.id,
- required this.username,
- required this.profileImageUrl,
- required this.acct,
- required this.note,
- });
-
- factory MastodonUser.fromJson(Map<String, dynamic> json) {
- return MastodonUser(
- id: json['id'] as String,
- username: json['username'] as String? ?? '',
- acct: json['acct'] as String,
- note: json['note'] as String,
- profileImageUrl: json['avatar'] as String? ?? ''
- );
- }
-}
-
-class PinnedPost {
- final String id;
- final String content;
-
- PinnedPost({required this.id, required this.content});
-
- factory PinnedPost.fromJson(Map<String, dynamic> json) {
- return PinnedPost(
- id: json['id'] as String,
- content: json['content'] as String,
- );
- }
-}
diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart
index aa7cff5a..a993afbb 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<NewSendPage> {
_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<void> _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<NewSendPage> {
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<NewSendPage> {
_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<bool> showParsedAddressConfirmationAlert(
+ BuildContext context,
+ ParsedAddress parsedAddress,
+) async {
+ final confirmed = await showPopUp<bool>(
+ 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 1e897743..2120a9a1 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<NewSwapPage> {
Future<String> fetchParsedAddress(
BuildContext context, String domain, CryptoCurrency currency) async {
printV("$domain");
- final parsedAddress = await getIt.get<AddressResolver>().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 435154aa..e284ba34 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 e573de4f..5234c9c3 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 8d9164e5..00000000
--- 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<ProfilePointer?> 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<UserMetadata?> 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<UserMetadata?> _fetchInfoFromRelay(
- String relayUrl, String userPubKey, List<int> 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<String, dynamic>;
-
- 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<void> _showErrorDialog(
- BuildContext context, String title, String errorMessage) async {
- if (context.mounted) {
- await showPopUp<void>(
- 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<String?> _showRelayChoiceDialog(
- BuildContext context,
- List<String> relays,
- ) async {
- String? selectedRelay;
-
- if (context.mounted) {
- await showPopUp<void>(
- context: context,
- builder: (BuildContext dialogContext) {
- return Picker<String>(
- selectedAtIndex: 0,
- title: S.of(dialogContext).choose_relay,
- items: relays,
- onItemSelected: (String relay) => selectedRelay = relay,
- );
- },
- );
- }
-
- return selectedRelay;
- }
-}
diff --git a/lib/nostr/nostr_user.dart b/lib/nostr/nostr_user.dart
deleted file mode 100644
index 4b1694a1..00000000
--- a/lib/nostr/nostr_user.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-
-class UserMetadata {
- final String name;
- final String lnurl;
- final String email;
- final String picture;
- final String about;
- final String nip05;
- final String banner;
- final String website;
-
- UserMetadata({
- required this.name,
- required this.lnurl,
- required this.email,
- required this.picture,
- required this.about,
- required this.nip05,
- required this.banner,
- required this.website,
- });
-
- factory UserMetadata.fromJson(Map<String, dynamic> json) {
- return UserMetadata(
- name: json['name'] as String? ?? '',
- lnurl: json['lud06'] as String? ?? '',
- email: json['lud16'] as String? ?? '',
- picture: json['picture'] as String? ?? '',
- about: json['about'] as String? ?? '',
- nip05: json['nip05'] as String? ?? '',
- banner: json['banner'] as String? ?? '',
- website: json['website'] as String? ?? '',
- );
- }
-}
\ No newline at end of file
diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart
index 73c238b5..2db9a956 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<ExchangeCardState>();
final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
final _formKey = GlobalKey<FormState>();
@@ -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<AddressResolver>().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 07cb5625..e60140bd 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<ContactListBody> {
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 d5ca5fc0..b4ea348f 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<void> _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<void>(
builder: (_) => CurrencyPicker(
@@ -203,4 +222,29 @@ class ContactPage extends BasePage {
}
});
}
+
+ Future<bool> showParsedAddressConfirmationAlert(
+ BuildContext context,
+ ParsedAddress parsedAddress,
+ ) async {
+ final confirmed = await showPopUp<bool>(
+ 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 40fa6c5c..1dc2fe94 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<ExchangeCardState>();
final receiveKey = GlobalKey<ExchangeCardState>();
@@ -655,8 +655,8 @@ class ExchangePage extends BasePage {
Future<String> fetchParsedAddress(
BuildContext context, String domain, CryptoCurrency currency) async {
- final parsedAddress = await getIt.get<AddressResolver>().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 d886963b..a72aa79a 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 e2564d3f..00000000
--- 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<String> 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<String?>(
- 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<void>(
- 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 0472c0e5..7e4b3383 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<SendCard> with AutomaticKeepAliveClientMixin<S
controller: addressController,
onURIScanned: (uri) async {
output.resetParsedAddress();
- await output.fetchParsedAddress(context);
// Process the payment through the new flow
await _handlePaymentFlow(
@@ -620,7 +619,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
_justHandledPasteButton = true;
try {
output.resetParsedAddress();
- await output.fetchParsedAddress(context);
final address =
output.isParsedAddress ? output.extractedAddress : output.address;
@@ -643,7 +641,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
output.resetParsedAddress();
},
onSelectedContact: (contact) {
- output.loadContact(contact);
},
validator: validator,
selectedCurrency: sendViewModel.selectedCryptoCurrency,
@@ -1033,8 +1030,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
if (current.isEmpty) return;
if (_justHandledPasteButton || _lastHandledAddress == current) return;
- await output.fetchParsedAddress(context);
-
// If it's a URI with params, go through URI flow
if (current.contains('=')) {
try {
diff --git a/lib/src/screens/send/widgets/send_template_card.dart b/lib/src/screens/send/widgets/send_template_card.dart
index 2e4a3585..81ff5398 100644
--- a/lib/src/screens/send/widgets/send_template_card.dart
+++ b/lib/src/screens/send/widgets/send_template_card.dart
@@ -92,11 +92,9 @@ class SendTemplateCard extends StatelessWidget {
],
onPushPasteButton: (context) async {
template.output.resetParsedAddress();
- await template.output.fetchParsedAddress(context);
},
onPushAddressBookButton: (context) async {
template.output.resetParsedAddress();
- await template.output.fetchParsedAddress(context);
},
buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest,
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
diff --git a/lib/src/screens/settings/domain_lookups_page.dart b/lib/src/screens/settings/domain_lookups_page.dart
index 9240af61..e8528939 100644
--- a/lib/src/screens/settings/domain_lookups_page.dart
+++ b/lib/src/screens/settings/domain_lookups_page.dart
@@ -1,6 +1,7 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
+import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/settings/connection_sync_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -20,47 +21,21 @@ class DomainLookupsPage extends BasePage {
return Container(
padding: EdgeInsets.only(top: 10),
child: Column(
- children: [
- SettingsSwitcherCell(
- title: 'Twitter',
- value: _connectionsSyncViewModel.lookupTwitter,
- onValueChange: (_, bool value) => _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 86368210..5ec27cec 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<dynamic> 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<FilteredList> {
),
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 33c94a46..bcf17a4f 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 e98ae1c0..25c08ff6 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 140cfa97..96536528 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 732ca03c..b06e2acb 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 71313ba8..00000000
--- 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/store/yat/yat_store.dart b/lib/store/yat/yat_store.dart
deleted file mode 100644
index 860eecee..00000000
--- a/lib/store/yat/yat_store.dart
+++ /dev/null
@@ -1,284 +0,0 @@
-import 'package:cake_wallet/core/secure_storage.dart';
-import 'package:cw_core/transaction_history.dart';
-import 'package:cw_core/utils/print_verbose.dart';
-import 'package:cw_core/wallet_base.dart';
-import 'package:cw_core/balance.dart';
-import 'package:cw_core/transaction_info.dart';
-import 'package:cake_wallet/store/app_store.dart';
-import 'package:mobx/mobx.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'dart:async';
-
-part 'yat_store.g.dart';
-
-class YatLink {
- static const partnerId = ''; // 'CW';
- static const baseDevUrl = ''; // 'https://yat.fyi';
- static const baseReleaseUrl = ''; // 'https://y.at';
- static const signInSuffix = ''; // '/partner/$partnerId/link-email';
- static const createSuffix = ''; // '/create';
- static const managePath = ''; // '/partner/$partnerId/manage';
- static const queryParameter = ''; // '?address_json=';
- static const apiDevUrl = ''; // 'https://a.yat.fyi';
- static const apiReleaseUrl = ''; // 'https://a.y.at';
- static const requestDevUrl = ''; // 'https://a.yat.fyi/emoji_id/';
- static const requestReleaseUrl = ''; //'https://a.y.at/emoji_id/';
- static const startFlowUrl = ''; // 'https://www.y03btrk.com/4RQSJ/6JHXF/';
- static const isDevMode = true;
- static const tags = <String, List<String>>{"XMR" : ['0x1001', '0x1002'],
- "BTC" : ['0x1003'], "LTC" : ['0x1019']};
-
- static String get apiUrl => YatLink.isDevMode
- ? YatLink.apiDevUrl
- : YatLink.apiReleaseUrl;
-
- static String get emojiIdUrl => apiUrl + '/emoji_id/';
-
- static String get baseUrl => YatLink.isDevMode
- ? YatLink.baseDevUrl
- : YatLink.baseReleaseUrl;
-}
-
-Future<List<String>> fetchYatAddress(String emojiId, String ticker) async {
- throw Exception();
- //final url = YatLink.emojiIdUrl + emojiId + '/payment';
- //final response = await get(url);
-
- //if (response.statusCode != 200) {
- // throw YatException(text: response.body.toString());
- //}
-
- //final addresses = <String>[];
- //final currency = ticker.toUpperCase();
- //final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- //final result = responseJSON['result'] as Map<dynamic, dynamic>;
- //result.forEach((dynamic key, dynamic value) {
- // final tag = key as String ?? '';
- // final record = value as Map<String, dynamic>;
-
- // if (YatLink.tags[currency]?.contains(tag) ?? false) {
- // final address = record['address'] as String;
- // if (address?.isNotEmpty ?? false) {
- // addresses.add(address);
- // }
- // }
- //});
-
- //return addresses;
- return [];
-}
-
-Future<String> fetchYatAccessToken(String refreshToken) async {
- throw Exception();
- //try {
- // final url = YatLink.apiUrl + '/auth/token/refresh';
- // final bodyJson = json.encode({'refresh_token': refreshToken});
- // final response = await post(
- // url,
- // headers: <String, String>{
- // 'Content-Type': 'application/json',
- // 'Accept': '*/*'
- // },
- // body: bodyJson);
-
- // if (response.statusCode != 200) {
- // throw YatException(text: response.body.toString());
- // }
-
- // final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- // return responseJSON['access_token'] as String;
- //}catch(_) {
- // return '';
- //}
- return '';
-}
-
-Future<String> fetchYatApiKey(String accessKey) async {
- throw Exception();
- //try {
- // final url = YatLink.apiUrl + '/api_keys';
- // final bodyJson = json.encode({'name': 'CW'});
- // final response = await post(
- // url,
- // headers: <String, String>{
- // 'Authorization': 'Bearer $accessKey',
- // 'Content-Type': 'application/json',
- // 'Accept': '*/*'
- // },
- // body: bodyJson);
-
- // if (response.statusCode != 200) {
- // throw YatException(text: response.body.toString());
- // }
-
- // final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- // return responseJSON['api_key'] as String;
- //}catch(_) {
- // return '';
- //}
- return '';
-}
-
-Future<void> updateEmojiIdAddress(String emojiId, String address, String apiKey, WalletType type) async {
- throw Exception();
- //final url = YatLink.emojiIdUrl + emojiId;
- //final cur = walletTypeToCryptoCurrency(type);
- //final curFormatted = cur.toString().toUpperCase();
- //var tag = '';
-
- //if (type == WalletType.monero && !address.startsWith('4')) {
- // tag = YatLink.tags[curFormatted].last;
- //} else {
- // tag = YatLink.tags[curFormatted].first;
- //}
-
- //final bodyJson = json.encode({
- // 'insert': [{
- // 'data': address,
- // 'tag': tag
- // }]
- //});
- //final response = await patch(
- // url,
- // headers: <String, String>{
- // 'x-api-key': apiKey,
- // 'Content-Type': 'application/json',
- // 'Accept': '*/*'
- // },
- // body: bodyJson);
-
- //if (response.statusCode != 200) {
- // throw YatException(text: response.body.toString());
- //}
-}
-
-Future<String> visualisationForEmojiId(String emojiId) async {
- throw Exception();
- //final url = YatLink.emojiIdUrl + emojiId + '/json/VisualizerFileLocations';
- //final response = await get(url);
- //final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- //final data = responseJSON['data'] as Map<String, dynamic>;
- //final result = data['gif'] as String ?? '';
- //return result;
- return '';
-}
-
-class YatStore = YatStoreBase with _$YatStore;
-
-abstract class YatStoreBase with Store {
- YatStoreBase({
- required this.appStore,
- required this.secureStorage})
- : _wallet = appStore.wallet,
- emoji = appStore.wallet?.walletInfo?.yatEmojiId ?? '',
- refreshToken = '',
- accessToken = '',
- apiKey = '',
- emojiIncommingSC = StreamController<String>.broadcast() {
- //reaction((_) => appStore.wallet, _onWalletChange);
- //reaction((_) => emoji, (String _) => _onEmojiChange());
- //reaction((_) => refreshToken, (String _) => _onRefreshTokenChange());
- }
-
- static const yatRefreshTokenKeyBase = 'yat_refresh_token';
- static const yatAccessTokenKeyBase = 'yat_access_token';
- static const yatApiKeyBase = 'yat_api_key';
-
- static String yatRefreshTokenKey(String name) => '${yatRefreshTokenKeyBase}_$name';
- static String yatAccessTokenKey(String name) => '${yatAccessTokenKeyBase}_$name';
- static String yatApiKey(String name) => '${yatApiKeyBase}_$name';
-
- AppStore appStore;
-
- SecureStorage secureStorage;
-
- @observable
- String emoji;
-
- @observable
- String refreshToken;
-
- @observable
- String accessToken;
-
- @observable
- String apiKey;
-
- StreamController<String> emojiIncommingSC;
-
- Stream<String> get emojiIncommingStream => emojiIncommingSC.stream;
-
- @observable
- WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
- _wallet;
-
- Future<void> init() async {
- if (_wallet == null) {
- return;
- }
-
- refreshToken = await secureStorage.read(key: yatRefreshTokenKey(_wallet!.walletInfo.name)) ?? '';
- accessToken = await secureStorage.read(key: yatAccessTokenKey(_wallet!.walletInfo.name)) ?? '';
- apiKey = await secureStorage.read(key: yatApiKey(_wallet!.walletInfo.name)) ?? '';
- }
-
- @action
- void _onWalletChange(
- WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
- TransactionInfo>
- wallet) {
- this._wallet = wallet;
- emoji = wallet?.walletInfo?.yatEmojiId ?? '';
- init();
- }
-
- @action
- void _onEmojiChange() {
- try {
- final walletInfo = _wallet?.walletInfo;
-
- if (walletInfo == null) {
- return;
- }
-
- walletInfo!.yatEid = emoji;
-
- walletInfo!.save();
- } catch (e) {
- printV(e.toString());
- }
- }
-
- @action
- Future<void> _onRefreshTokenChange() async {
- throw Exception();
-
- //try {
- // await secureStorage.write(key: yatRefreshTokenKey(_wallet.walletInfo.name), value: refreshToken);
- // accessToken = await fetchYatAccessToken(refreshToken);
- // await secureStorage.write(key: yatAccessTokenKey(_wallet.walletInfo.name), value: accessToken);
- // apiKey = await fetchYatApiKey(accessToken);
- // await secureStorage.write(key: yatApiKey(_wallet.walletInfo.name), value: accessToken);
- //} catch (e) {
- // printV(e.toString());
- //}
- }
-
- String defineQueryParameters() {
- throw Exception();
- //final result = <String, String>{};
- //final tags = YatLink.tags[_wallet.currency.toString().toUpperCase()];
- //String tag = tags.first;
-
- //if (_wallet.type == WalletType.monero
- // && _wallet.walletAddresses.address.startsWith('4')) {
- // tag = tags.last;
- //}
- //result[tag] = '${_wallet.walletAddresses.address}|${_wallet.name}';
- //final addressJson = json.encode([result]);
- //final addressJsonBytes = utf8.encode(addressJson);
-
- //return base64.encode(addressJsonBytes);
- return '';
- }
-}
diff --git a/lib/twitter/twitter_api.dart b/lib/twitter/twitter_api.dart
deleted file mode 100644
index a508b3ed..00000000
--- a/lib/twitter/twitter_api.dart
+++ /dev/null
@@ -1,67 +0,0 @@
-import 'dart:convert';
-
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-import 'package:cake_wallet/twitter/twitter_user.dart';
-import 'package:cw_core/utils/proxy_wrapper.dart';
-
-class TwitterApi {
- static const twitterBearerToken = secrets.twitterBearerToken;
- static const httpsScheme = 'https';
- static const apiHost = 'api.twitter.com';
- static const userPath = '/2/users/by/username/';
-
- static Future<TwitterUser> lookupUserByName({required String userName}) async {
- final queryParams = {
- 'user.fields': 'description,profile_image_url,location',
- 'expansions': 'pinned_tweet_id',
- 'tweet.fields': 'note_tweet'
- };
- final headers = {'authorization': 'Bearer $twitterBearerToken'};
- final uri = Uri(
- scheme: httpsScheme,
- host: apiHost,
- path: userPath + userName,
- queryParameters: queryParams);
-
- final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers).catchError((error) {
- throw Exception('HTTP request failed: $error');
- });
-
- if (response.statusCode != 200) {
- throw Exception('Unexpected http status: ${response.statusCode}');
- }
-
-
- final Map<String, dynamic> responseJSON = jsonDecode(response.body) as Map<String, dynamic>;
- if (responseJSON['errors'] != null &&
- !responseJSON['errors'][0]['detail']
- .toString()
- .contains("Could not find tweet with pinned_tweet_id")) {
- throw Exception(responseJSON['errors'][0]['detail']);
- }
-
- return TwitterUser.fromJson(responseJSON, _getPinnedTweet(responseJSON));
- }
-
- static Tweet? _getPinnedTweet(Map<String, dynamic> responseJSON) {
- try {
- final tweetId = responseJSON['data']['pinned_tweet_id'] as String?;
- if (tweetId == null || responseJSON['includes'] == null) return null;
-
- final tweetIncludes = List.from(responseJSON['includes']['tweets'] as List);
- final pinnedTweetData = tweetIncludes.firstWhere(
- (tweet) => tweet['id'] == tweetId,
- orElse: () => null,
- ) as Map<String, dynamic>?;
-
- if (pinnedTweetData == null) return null;
-
- final pinnedTweetText =
- (pinnedTweetData['note_tweet']?['text'] ?? pinnedTweetData['text']) as String;
-
- return Tweet(id: tweetId, text: pinnedTweetText);
- } catch (e) {
- return null;
- }
- }
-}
diff --git a/lib/twitter/twitter_user.dart b/lib/twitter/twitter_user.dart
deleted file mode 100644
index f124de40..00000000
--- a/lib/twitter/twitter_user.dart
+++ /dev/null
@@ -1,49 +0,0 @@
-class TwitterUser {
- TwitterUser(
- {required this.id,
- required this.username,
- required this.name,
- required this.description,
- required this.location,
- required this.profileImageUrl,
- this.pinnedTweet});
-
- final String id;
- final String username;
- final String name;
- final String description;
- final String location;
- final String profileImageUrl;
- final Tweet? pinnedTweet;
-
- factory TwitterUser.fromJson(Map<String, dynamic> json, [Tweet? pinnedTweet]) {
- final profileImageUrl = json['data']['profile_image_url'] as String? ?? '';
- final scaledProfileImageUrl = profileImageUrl.replaceFirst('normal', '200x200');
- return TwitterUser(
- id: json['data']['id'] as String,
- username: json['data']['username'] as String? ?? '',
- name: json['data']['name'] as String,
- description: json['data']['description'] as String? ?? '',
- location: json['data']['location'] as String? ?? '',
- profileImageUrl: scaledProfileImageUrl,
- pinnedTweet: pinnedTweet,
- );
- }
-}
-
-class Tweet {
- Tweet({
- required this.id,
- required this.text,
- });
-
- final String id;
- final String text;
-
- factory Tweet.fromJson(Map<String, dynamic> json) {
- return Tweet(
- id: json['id'] as String,
- text: json['text'] as String,
- );
- }
-}
diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart
index c808f966..d0d74b95 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<void> extractParsedAddress(BuildContext context) async{
- if(currency == null) return;
- final parsedAddress = await getIt.get<AddressResolver>().resolve(context, address, currency!);
- if(parsedAddress.name.isNotEmpty) {
- displayName = parsedAddress.name;
+ Future<ParsedAddress?> 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<void> save() async {
diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart
index d97d119a..f9d355c5 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 4217aab5..732729d6 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 543a541b..ef460938 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<String, dynamic> get extra {
final fields = <String, dynamic>{};
- 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<void> fetchParsedAddress(BuildContext context) async {
- final domain = address;
+ @action
+ void loadContact(ContactBase contact) {
final currency = cryptoCurrencyHandler();
- parsedAddress = await getIt.get<AddressResolver>().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;
- }
- void loadContact(ContactBase contact) {
- address = contact.name;
- parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name);
- extractedAddress = parsedAddress.addresses.first;
- note = parsedAddress.description;
+ 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 5c4b2c09..61990c19 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<ParsedAddress?> 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<Template> get templates => sendTemplateViewModel.templates
@@ -454,6 +480,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final HardwareWalletViewModel? hardwareWalletViewModel;
final FeesViewModel feesViewModel;
final FiatConversionStore _fiatConversationStore;
+ final AddressResolverService _adrResService;
final Box<TransactionDescription> transactionDescriptionBox;
@computed
@@ -606,8 +633,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
clearOutputs();
outputs.first.address = paymentRequest.address;
- outputs.first.parsedAddress =
- ParsedAddress(addresses: [paymentRequest.address], name: ocpRequest!.receiverName);
+ outputs.first.parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {currency:paymentRequest.address}, handle: ocpRequest!.receiverName);
outputs.first.setCryptoAmount(paymentRequest.amount);
outputs.first.note = ocpRequest!.receiverName;
@@ -1342,7 +1368,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
for (final output in outputs) {
final address =
- output.isParsedAddress ? output.parsedAddress.addresses.first : output.address;
+ output.isParsedAddress ? output.parsedAddress.parsedAddressByCurrencyMap[selectedCryptoCurrency] ?? '' : output.address;
if (address.isNotEmpty &&
!contactAddresses.contains(address) &&
diff --git a/lib/view_model/settings/connection_sync_view_model.dart b/lib/view_model/settings/connection_sync_view_model.dart
index 1d1fb30f..9d6da9ed 100644
--- a/lib/view_model/settings/connection_sync_view_model.dart
+++ b/lib/view_model/settings/connection_sync_view_model.dart
@@ -1,5 +1,7 @@
import 'dart:async';
+import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
+import 'package:cake_wallet/core/address_resolver/address_sources.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/evm/evm.dart';
@@ -55,6 +57,24 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
+ @computed
+ bool get lookupsNostr => _settingsStore.lookupsNostr;
+
+ @computed
+ bool get lookupsFio => _settingsStore.lookupsFio;
+
+ @computed
+ bool get lookupsBip353 => _settingsStore.lookupsBip353;
+
+ @computed
+ bool get lookupsThorChain => _settingsStore.lookupsThorChain;
+
+ @computed
+ bool get lookupsZcashAddress => _settingsStore.lookupsZcashAddress;
+
+ @computed
+ bool get lookupsLNUrlPay => _settingsStore.lookupsLNUrl;
+
@computed
ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus;
@@ -68,6 +88,99 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get builtinTor => _settingsStore.currentBuiltinTor;
+ List<AddressSource> get domainLookupSources => AddressResolverService.supportedSources;
+
+ bool lookupValue(AddressSource source) {
+ switch (source) {
+ case AddressSource.twitter:
+ return lookupTwitter;
+ case AddressSource.mastodon:
+ return looksUpMastodon;
+ case AddressSource.yatRecord:
+ return looksUpYatService;
+ case AddressSource.unstoppableDomains:
+ return looksUpUnstoppableDomains;
+ case AddressSource.openAlias:
+ return looksUpOpenAlias;
+ case AddressSource.ens:
+ return looksUpENS;
+ case AddressSource.zcashName:
+ return lookupsZcashNames;
+ case AddressSource.zcashAddress:
+ return lookupsZcashAddress;
+ case AddressSource.wellKnown:
+ return looksUpWellKnown;
+ case AddressSource.zanoAlias:
+ return lookupsZanoAlias;
+ case AddressSource.bip353:
+ return lookupsBip353;
+ case AddressSource.fio:
+ return lookupsFio;
+ case AddressSource.lnurlPay:
+ return lookupsLNUrlPay;
+ case AddressSource.thorChain:
+ return lookupsThorChain;
+ case AddressSource.nostr:
+ return lookupsNostr;
+ case AddressSource.contact:
+ case AddressSource.notParsed:
+ return false;
+ }
+ }
+
+ @action
+ void setLookupValue(AddressSource source, bool value) {
+ switch (source) {
+ case AddressSource.twitter:
+ _settingsStore.lookupsTwitter = value;
+ break;
+ case AddressSource.mastodon:
+ _settingsStore.lookupsMastodon = value;
+ break;
+ case AddressSource.yatRecord:
+ _settingsStore.lookupsYatService = value;
+ break;
+ case AddressSource.unstoppableDomains:
+ _settingsStore.lookupsUnstoppableDomains = value;
+ break;
+ case AddressSource.openAlias:
+ _settingsStore.lookupsOpenAlias = value;
+ break;
+ case AddressSource.ens:
+ _settingsStore.lookupsENS = value;
+ break;
+ case AddressSource.zcashName:
+ _settingsStore.lookupsZcashNames = value;
+ break;
+ case AddressSource.zcashAddress:
+ _settingsStore.lookupsZcashAddress = value;
+ break;
+ case AddressSource.wellKnown:
+ _settingsStore.lookupsWellKnown = value;
+ break;
+ case AddressSource.zanoAlias:
+ _settingsStore.lookupsZanoAlias = value;
+ break;
+ case AddressSource.bip353:
+ _settingsStore.lookupsBip35Why this scored 39/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.