Address resolver refactoring (#3291)

* restructure address resolver sources

* refactor address resolver into lookup service

* refactor address resolver to provider pattern

* use addressSource to drive providers & settings

* error handling

* add address source icons

* Update lib/core/address_validator.dart [skip ci]

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* review fixes

* add validations to extractParsedAddress

* skip address resolver for valid addresses

* minor fix [skip ci]

* fix formattedName x[skip ci]

Co-authored-by: malik1004x <malikowskirobert@gmail.com>

* fix formatted name zano [skip ci]

Co-authored-by: malik1004x <malikowskirobert@gmail.com>

* address PR review comments

* add icons for new address source resolvers

* confirm parsed addresses in contacts

* minor fixes

* LNUrl for BTCLN only and contact name handling

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: malik1004x <malikowskirobert@gmail.com>
This commit is contained in:
Serhii
2026-07-06 16:48:30 +03:00
committed by GitHub
co-authored by Omar Hatem malik1004x
parent 00b7d7ce0d
commit 7928c912e8
93 changed files with 3044 additions and 1796 deletions
@@ -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 {
@@ -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';
@@ -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,
+36 -29
View File
@@ -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;
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;
}
@action
void loadContact(ContactBase contact) {
address = contact.name;
parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name);
extractedAddress = parsedAddress.addresses.first;
note = parsedAddress.description;
final currency = cryptoCurrencyHandler();
address = contact.address;
applyAddressLookupResult(
ParsedAddress(
parsedAddressByCurrencyMap: {currency: contact.address},
addressSource: AddressSource.contact,
handle: contact.name,
profileName: contact.name,
),
);
}
}
+38 -12
View File
@@ -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) &&
@@ -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.lookupsBip353 = value;
break;
case AddressSource.fio:
_settingsStore.lookupsFio = value;
break;
case AddressSource.lnurlPay:
_settingsStore.lookupsLNUrl = value;
break;
case AddressSource.thorChain:
_settingsStore.lookupsThorChain = value;
break;
case AddressSource.nostr:
_settingsStore.lookupsNostr = value;
break;
case AddressSource.contact:
case AddressSource.notParsed:
break;
}
}
@computed
bool get hasPowNodes => [WalletType.nano, WalletType.banano].contains(_wallet.type);
@@ -119,32 +232,6 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get canUseTronGrid => _wallet.type == WalletType.tron;
@action
void setLookupsTwitter(bool value) => _settingsStore.lookupsTwitter = value;
@action
void setLookupsZanoAlias(bool value) => _settingsStore.lookupsZanoAlias = value;
@action
void setLookupsMastodon(bool value) => _settingsStore.lookupsMastodon = value;
@action
void setLookupsENS(bool value) => _settingsStore.lookupsENS = value;
@action
void setLookupsZcashNames(bool value) => _settingsStore.lookupsZcashNames = value;
@action
void setLookupsWellKnown(bool value) => _settingsStore.lookupsWellKnown = value;
@action
void setLookupsYatService(bool value) => _settingsStore.lookupsYatService = value;
@action
void setLookupsUnstoppableDomains(bool value) => _settingsStore.lookupsUnstoppableDomains = value;
@action
void setLookupsOpenAlias(bool value) => _settingsStore.lookupsOpenAlias = value;
@action
void setUseMempoolFeeAPI(bool value) => _settingsStore.useMempoolFeeAPI = value;
@@ -2,6 +2,7 @@ import 'dart:core';
import 'dart:developer' as dev;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
@@ -17,7 +18,6 @@ import 'package:cake_wallet/reactions/wallet_utils.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/utils/list_item.dart';
import 'package:cake_wallet/utils/qr_util.dart';