Merge branch 'dev' into cw-1497-transaction-history-improved-layout

This commit is contained in:
Robert Malikowski
2026-07-13 15:35:49 +02:00
137 changed files with 3860 additions and 2415 deletions
@@ -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 ?? '',
),
@@ -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,
});
}
@@ -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 [];
}
}
}
@@ -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();
}
}
@@ -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,
);
}
}
@@ -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 [];
}
}
}
@@ -2,13 +2,9 @@ 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({
@@ -104,69 +100,4 @@ class Bip353Record {
}
}
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';
}
}
}
@@ -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 [];
}
}
@@ -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,
};
}
@@ -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;
}
}
@@ -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 [];
}
}
}
@@ -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 [];
}
}
}
@@ -1,7 +1,7 @@
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';
import 'package:cake_wallet/mastodon/mastodon_user.dart';
class MastodonAPI {
static const httpsScheme = 'https';
@@ -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 [];
}
}
}
@@ -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 '';
}
}
}
@@ -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 [];
}
}
}
@@ -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);
}
}
@@ -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;
}
@@ -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 [];
}
}
}
@@ -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,
);
}
}
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/twitter/twitter_user.dart';
import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class TwitterApi {
@@ -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;
}
}
@@ -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 [];
}
}
}
@@ -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;
}
}
@@ -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 [];
}
}
}
@@ -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)';
}
@@ -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 [];
}
}
}
@@ -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;
}
}
}
@@ -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 [];
}
}
}
@@ -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;
}
}
+56 -31
View File
@@ -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;
}
}
}
+3 -2
View File
@@ -1,11 +1,12 @@
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com';
const _fiatApiOnionAuthority = 'kfkyguqtz5vcnbvar5pjgddkaeawbo4j3r4fj3e22k3tzqageplosiid.onion';
const _fiatApiClearNetAuthority = 'prices.cakewallet.com';
const _fiatApiOnionAuthority = '46wisoe2uwipcj2j4og6smiq7hbmj34fkkrquwlzbbsqtgat7bf3erid.onion';
const _fiatApiPath = '/v2/rates';
Future<double> _fetchPrice(String crypto, String fiat, bool torOnly) async {
-55
View File
@@ -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;
}
}
}
+17 -10
View File
@@ -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));
-54
View File
@@ -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 "";
}
}
}
-65
View File
@@ -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;
}
}
-89
View File
@@ -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);
}
}
-549
View File
@@ -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;
}
}
-212
View File
@@ -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;
}
+6
View File
@@ -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';
@@ -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;
}
-93
View File
@@ -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);
}
}
-13
View File
@@ -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;
}
-29
View File
@@ -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;
}
}
+24 -22
View File
@@ -692,29 +692,18 @@ class CWEVM extends EVM {
);
}
Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
Erc20Token token,
BigInt balanceWei,
) async {
Future<double> _fetchFiatApiPriceForToken(Erc20Token token) async {
try {
final settingsStore = getIt.get<SettingsStore>();
final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
final price = await FiatConversionService.fetchPrice(
return await FiatConversionService.fetchPrice(
crypto: token,
fiat: FiatCurrency.usd,
torOnly: torOnly,
);
final hasValidFiatPrice = price > 0;
final decimals = token.decimal;
final balance = balanceWei.toDouble() / math.pow(10, decimals);
final usdValue = balance * price;
return (usdValue: usdValue, hasValidFiatPrice: hasValidFiatPrice);
} catch (e) {
return (usdValue: 0.0, hasValidFiatPrice: false);
} catch (_) {
return 0.0;
}
}
@@ -730,21 +719,34 @@ class CWEVM extends EVM {
final List<Future<void>> tokenChecks = [];
final whitelistedContracts =
wallet.getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
for (final item in result.newTokens) {
tokenChecks.add((() async {
final token = item.token;
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
final isWhitelisted = whitelistedContracts.contains(token.contractAddress.toLowerCase());
final fiatResult = await _getTokenUsdValueAndFiatCheck(
token,
item.balanceWei,
);
final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
final moralisPrice = item.moralisUsdPrice;
final moralisValue = item.moralisUsdValue ?? 0.0;
final hasMoralisPrice = moralisPrice != null && moralisPrice > 0;
final fiatApiPrice = await _fetchFiatApiPriceForToken(token);
final hasFiatApiPrice = fiatApiPrice > 0;
final isImpersonator =
hasFiatApiPrice && !hasMoralisPrice && !isWhitelisted && !item.verifiedContract;
final isSpam = isPropertiesSuspicious ||
token.isPotentialScam ||
isImpersonator ||
(!hasMoralisPrice && !hasFiatApiPrice);
token.isPotentialScam = isSpam;
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
token.enabled =
hasMoralisPrice && hasFiatApiPrice && (moralisValue >= _minTokenUsdValue) && !isSpam;
await wallet.addErc20Token(token);
})());
@@ -29,6 +29,8 @@ class ChainflipExchangeProvider extends ExchangeProvider {
CryptoCurrency.arbEth,
CryptoCurrency.usdcArb,
CryptoCurrency.usdtArb,
CryptoCurrency.trx,
CryptoCurrency.usdttrc20,
];
static const _baseURL = 'chainflip-broker.io';
@@ -315,6 +317,11 @@ class ChainflipExchangeProvider extends ExchangeProvider {
}
String _normalizeCurrency(CryptoCurrency currency) {
// Chainflip uses 'tron' as the network slug, but Cake uses tag 'TRX' (and null
// for native TRX), so these can't go through the generic title.tag logic below.
if (currency == CryptoCurrency.trx) return 'trx.tron';
if (currency == CryptoCurrency.usdttrc20) return 'usdt.tron';
final tag = currency.tag?.toLowerCase();
final title = currency.title.toLowerCase();
@@ -330,6 +337,7 @@ class ChainflipExchangeProvider extends ExchangeProvider {
'ETHEREUM' => 'Ethereum',
'ARBITRUM' => 'Arbitrum',
'SOLANA' => 'Solana',
'TRON' => 'Tron',
_ => name
};
@@ -350,6 +358,8 @@ class ChainflipExchangeProvider extends ExchangeProvider {
'eth.arb' => CryptoCurrency.arbEth,
'usdc.arb' => CryptoCurrency.usdcArb,
'usdt.arb' => CryptoCurrency.usdtArb,
'trx.tron' => CryptoCurrency.trx,
'usdt.tron' => CryptoCurrency.usdttrc20,
_ => null
};
@@ -575,10 +575,17 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
final token = supported.firstWhereOrNull((t) => t.assetId == assetId);
if (token == null) return null;
final title = token.symbol;
final title = token.symbol.toUpperCase()
.replaceAll(RegExp(r'\s*\([^)]*\)'), '');
final normalizedNetwork = _normalizeNearBlockchainToTag(token.blockchain);
final tag =
normalizedNetwork == title.toUpperCase() ? null : normalizedNetwork;
final isNativeAsset = assetId.contains(':native:coin');
final tag = isNativeAsset || normalizedNetwork == title
? null
: normalizedNetwork;
return (title, tag);
}
+52 -7
View File
@@ -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;
}
+8 -5
View File
@@ -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 {
@@ -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';
@@ -5,13 +5,12 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import "package:cw_core/wallet_type.dart";
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
class L2SendExternalModal extends StatefulWidget {
const L2SendExternalModal({super.key, required this.sendViewModel});
@@ -180,11 +179,7 @@ class _L2SendExternalModalState extends State<L2SendExternalModal> {
children: [
Flexible(
child: NewPrimaryButton(
onPressed:(){
Clipboard.setData(
ClipboardData(text: output.address),
);
},
onPressed: () => ClipboardUtil.copyToClipboard(context, output.address),
text: S.of(context).copy,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary),
@@ -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;
@@ -4,11 +4,11 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_modal_header.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class SwapSendExternalModal extends StatefulWidget {
const SwapSendExternalModal(
@@ -45,7 +45,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
@override
Widget build(BuildContext context) {
if (uri == null) return SizedBox.shrink();
if (widget.address.isEmpty) return SizedBox.shrink();
final resolvedSize = MediaQuery.of(context).size.width * (largeQrMode ? 0.8 : 0.54);
return PopScope(
@@ -139,7 +139,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
child: QrImage(
size: animatedSize,
embeddedImagePath: widget.from.iconPath,
data: uri.toString(),
data: uri?.toString() ?? widget.address,
),
),
);
@@ -175,7 +175,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
children: [
Flexible(
child: NewPrimaryButton(
onPressed: ()=> Clipboard.setData(ClipboardData(text:widget.address)),
onPressed: () => ClipboardUtil.copyToClipboard(context, widget.address),
text: S.of(context).copy,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary),
-150
View File
@@ -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;
}
}
+3 -1
View File
@@ -387,9 +387,11 @@ class CWSolana extends Solana {
tokenChecks.add((() async {
final token = item.token;
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
final fiatResult = await _getTokenUsdValueAndFiatCheck(token, item.balance);
final isSpam = !fiatResult.hasValidFiatPrice;
final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
token.isPotentialScam = isSpam;
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
+30 -12
View File
@@ -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] ?? ''
: '';
}
}
@@ -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 =
+48 -4
View File
@@ -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;
}
}
@@ -148,6 +148,15 @@ class CakeFeaturesPage extends StatelessWidget {
subTitle: "Turn your old phone into your new hardware wallet with our new app",
image: 'assets/images/cupcake.png',
),
AppsWidget(
isWide: true,
isLink: true,
isCake: true,
onTap: () => _launchUrl("radar.chat"),
title: "Radar",
subTitle: "Your messages. Your Bitcoin. Together, at last",
image: 'assets/images/radar.png',
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.only(left: 24, top: 16, bottom: 8),
+5 -5
View File
@@ -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 {
@@ -139,68 +139,71 @@ class WalletTypeFormState extends State<WalletTypeForm> {
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(left: 18, right: 18, bottom: 12),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: ListView.separated(
key: const ValueKey('new_wallet_type_scrollable_key'),
itemCount: filteredTypes.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
final item = filteredTypes[index];
return GestureDetector(
key: ValueKey('new_wallet_type_${item.name}_button_key'),
behavior: HitTestBehavior.opaque,
onTap: () {
onTypeSelected(item);
},
child: SizedBox(
height: 48,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CakeImageWidget(
height: 24,
width: 24,
imageUrl: getCryptoCurrencyIconForWalletListItem(item),
),
const SizedBox(width: 12),
Text(walletTypeToDisplayName(item)),
const SizedBox(width: 4),
Text(
walletTypeToDisplayTicker(item),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: ListView.separated(
key: const ValueKey('new_wallet_type_scrollable_key'),
itemCount: filteredTypes.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
final item = filteredTypes[index];
return GestureDetector(
key: ValueKey('new_wallet_type_${item.name}_button_key'),
behavior: HitTestBehavior.opaque,
onTap: () {
onTypeSelected(item);
},
child: SizedBox(
height: 50,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CakeImageWidget(
height: 24,
width: 24,
imageUrl: getCryptoCurrencyIconForWalletListItem(item),
),
)
],
),
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_forward.svg",
height: 16,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
const SizedBox(width: 12),
Text(walletTypeToDisplayName(item)),
const SizedBox(width: 4),
Text(
walletTypeToDisplayTicker(item),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
)
],
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_forward.svg",
height: 16,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
)
],
),
),
),
),
);
},
separatorBuilder: (context, index) => Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
);
},
separatorBuilder: (context, index) => Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
),
),
),
-1
View File
@@ -780,7 +780,6 @@ class SendPage extends BasePage {
}
output.resetParsedAddress();
await output.fetchParsedAddress(context);
}
Output _defineCurrentOutput() {
@@ -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;
}
@@ -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 {
@@ -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(
@@ -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(),
),
);
}),
@@ -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) {
+1 -1
View File
@@ -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';
+2 -4
View File
@@ -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),
),
),
);
@@ -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}';
+67 -2
View File
@@ -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,
-10
View File
@@ -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';
}
+18 -2
View File
@@ -1,6 +1,8 @@
import 'package:flutter/services.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sensitive_clipboard/sensitive_clipboard.dart';
class ClipboardUtil {
@@ -12,4 +14,18 @@ class ClipboardUtil {
return Clipboard.setData(data);
}
/// Copies [text] to the clipboard and shows a "Copied to Clipboard"
/// confirmation toast. Prefer this for any user-facing copy action so the
/// feedback stays consistent across the app.
static Future<void> copyToClipboard(
BuildContext context,
String text, {
bool isSensitive = false,
}) async {
await setSensitiveDataToClipboard(ClipboardData(text: text), isSensitive: isSensitive);
if (context.mounted) {
showBar<void>(context, S.of(context).copied_to_clipboard);
}
}
}
+18 -5
View File
@@ -299,14 +299,15 @@ class ExceptionHandler {
"FocusScopeNode was used after being disposed",
"_getDismissibleFlushbar",
"_QueuedFuture.execute (package:universal_ble/src/queue.dart:65)",
"Pending Request Canceled | RequestQueue disposed",
"reown_core/relay_client/websocket/websocket_handler.dart",
"Image upload failed due to loss of GPU access",
"transport error",
"SdkError.sparkError(field0: Operator RPC error: Connection error: status: Unavailable, message: \"dns error\", details: []",
"the timeout of the request was reached",
"support for coin removed, your seedphrase:"
"Exception: Invalid image data"
"support for coin removed, your seedphrase:",
"Exception: Invalid image data",
];
static Future<void> _addDeviceInfo(File file) async {
@@ -441,8 +442,20 @@ class ExceptionHandler {
}
static bool _flutterErrorIgnore(FlutterErrorDetails errorDetails) {
// most probably a flutter context error so just ignore it if there is no stack we can debug with
return errorDetails.exception.toString().contains("Null check operator used on a null value") &&
errorDetails.stack == null;
if (errorDetails.exception.toString().contains("Null check operator used on a null value")) {
// Most probably a flutter context error so just ignore it if there is no
// stack we can debug with.
if (errorDetails.stack == null) {
return true;
}
final stack = errorDetails.stack.toString();
if (stack.contains("handleFocusHighlightModeChange") ||
stack.contains("_HighlightModeManager")) {
return true;
}
}
return false;
}
}
@@ -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:cake_wallet/view_model/dashboard/date_section_item.dart';
import "package:cw_core/balance_card_style_settings.dart";
@@ -36,7 +37,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';
@@ -55,6 +55,12 @@ class TransactionListItem extends ActionListItem with Keyable {
}
String get formattedTitle {
if (balanceViewModel.wallet.type == WalletType.bitcoin &&
transaction.additionalInfo['hasMissingInputTx'] == true) {
return 'Transaction has missing data';
}
if (transaction.additionalInfo['autoShield'] == true) {
return "Autoshield";
}
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
@@ -24,6 +25,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
import 'package:cake_wallet/utils/qr_util.dart';
import 'package:cake_wallet/utils/token_utilities.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
@@ -48,7 +50,6 @@ abstract class ExchangeTradeViewModelBase with Store {
required this.feesViewModel,
required this.fiatConversionStore,
}) : trade = tradesStore.trade!,
isSendable = _checkIfCanSend(tradesStore, wallet),
isSwapsXYZCanSendFromExternal = _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet),
items = ObservableList<ExchangeTradeItem>() {
setUpOutput();
@@ -115,12 +116,10 @@ abstract class ExchangeTradeViewModelBase with Store {
@observable
Trade trade;
@observable
bool isSendable;
bool isSwapsXYZCanSendFromExternal;
bool get isSendable => checkIfCanSend(trade, wallet) == null;
/// Providers that should hide the "send from external" button
static const List<Type> _providersThatHideExternalSend = [
JupiterExchangeProvider,
@@ -212,7 +211,13 @@ abstract class ExchangeTradeViewModelBase with Store {
@action
Future<void> confirmSending() async {
if (!isSendable) return;
final canSendError = checkIfCanSend(trade, wallet);
if (canSendError != null) {
_logCanSendError(trade, wallet, canSendError);
sendViewModel.state = FailureState(canSendError);
return;
}
final selected = trade.from;
if (selected == null) {
@@ -348,48 +353,56 @@ abstract class ExchangeTradeViewModelBase with Store {
);
}
static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) {
final trade = tradesStore.trade!;
String? checkIfCanSend(Trade? trade, WalletBase wallet) {
if (trade == null) return 'Trade is null';
final tradeFrom = trade.from;
if (tradeFrom == null) return 'Trade from currency is null';
bool _sameCurrency(CryptoCurrency? a, CryptoCurrency? b) =>
a != null && b != null && a.titleAndTagEqual(b);
bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b);
bool _isEthToken() =>
wallet.currency == CryptoCurrency.eth && tradeFrom?.tag == CryptoCurrency.eth.title;
bool _isTokenBelongingToWallet(CryptoCurrency cur) {
final chainTag = cur.tag ?? cur.title;
return wallet.currency == cur &&
(tradeFrom.tag?.toUpperCase() == chainTag.toUpperCase() ||
tradeFrom.title.toUpperCase() == chainTag.toUpperCase());
}
bool _isPolygonToken() =>
wallet.currency == CryptoCurrency.maticpoly &&
tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
bool _isBaseToken() =>
wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag;
bool _isArbitrumToken() =>
wallet.currency == CryptoCurrency.arbEth &&
(tradeFrom?.tag == CryptoCurrency.arbEth.tag ||
tradeFrom?.title == CryptoCurrency.arbEth.tag); // This is to handle the CryptoCurrency.arb that doesn't have a tag but fully belongs to the Arbitrum chain
bool _isTronToken() =>
wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title;
bool _isSplToken() =>
wallet.currency == CryptoCurrency.sol && tradeFrom?.tag == CryptoCurrency.sol.title;
bool _isBscToken() =>
wallet.currency == CryptoCurrency.bnb && tradeFrom?.tag == CryptoCurrency.bnb.tag;
return _sameCurrency(tradeFrom, wallet.currency) ||
final canSend = _sameCurrency(tradeFrom, wallet.currency) ||
(_sameCurrency(tradeFrom, CryptoCurrency.btcln) &&
wallet.currency == CryptoCurrency.btc) ||
tradesStore.trade!.provider == ExchangeProviderDescription.xmrto ||
_isEthToken() ||
_isPolygonToken() ||
_isSplToken() ||
_isTronToken() ||
_isBaseToken() ||
_isArbitrumToken() ||
_isBscToken();
trade.provider == ExchangeProviderDescription.xmrto ||
_isTokenBelongingToWallet(CryptoCurrency.eth) ||
_isTokenBelongingToWallet(CryptoCurrency.maticpoly) ||
_isTokenBelongingToWallet(CryptoCurrency.baseEth) ||
_isTokenBelongingToWallet(CryptoCurrency.arbEth) ||
_isTokenBelongingToWallet(CryptoCurrency.trx) ||
_isTokenBelongingToWallet(CryptoCurrency.sol) ||
_isTokenBelongingToWallet(CryptoCurrency.bnb);
if (!canSend) {
return 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.';
}
return null;
}
void _logCanSendError(Trade? trade, WalletBase wallet, String error) {
ExchangeProviderLogger.logError(
provider: trade?.provider,
function: '_checkIfCanSend',
error: error,
requestData: {
'tradeId': trade?.id,
'tradeFrom': trade?.from?.title,
'tradeFromTag': trade?.from?.tag,
'tradeTo': trade?.to?.title,
'tradeToTag': trade?.to?.tag,
'walletName': wallet.name,
'walletCurrency': wallet.currency.title,
'walletCurrencyTag': wallet.currency.tag,
},
);
}
static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) {
@@ -415,9 +428,13 @@ abstract class ExchangeTradeViewModelBase with Store {
return null;
}
// Using trade currency here so the external-send QR encodes the correct scheme
final uriWalletType =
cryptoCurrencyOrTokenToWalletType(fromCurrency) ?? wallet.type;
// Using the trade's `from` currency so the external-send QR encodes the correct scheme.
final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency);
// for other currencies that we don't have wallets for
if (uriWalletType == null) {
return ExternalAddressURI(address: inputAddress, amount: amount);
}
printV(uriWalletType);
@@ -45,6 +45,7 @@ import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/amount/money_double.dart';
import "package:cw_core/wallet_info.dart";
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
@@ -804,7 +805,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate);
_depositAmount = Money.tryParse(amount_.toStringAsFixed(depositCurrency.decimals), depositCurrency);
_depositAmount = amount_.tryToMoney(depositCurrency);
}
@action
@@ -863,8 +864,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate);
_receiveAmount =
Money.tryParse(amount_.toStringAsFixed(min(20, receiveCurrency.decimals)), receiveCurrency);
_receiveAmount = amount_.tryToMoney(receiveCurrency);
}
bool checkIfInputMeetsMinOrMaxCondition(String input) {
@@ -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;
@@ -9,7 +9,6 @@ import 'package:cake_wallet/zano/zano.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
@@ -51,10 +50,9 @@ String _moneroRecipientAddressForDisplay(String raw, WalletType walletType) {
return match?.group(0) ?? raw.trim();
}
bool isLightning(TransactionInfo tx) {
printV(tx.additionalInfo);
return (tx.additionalInfo["isLightning"] as bool?) ?? false;
}
bool isLightning(TransactionInfo tx) => (tx.additionalInfo["isLightning"] as bool?) ?? false;
bool hasLightningPreimage(TransactionInfo tx) => (tx.additionalInfo["preimage"] as String?) != null;
class TxDetailRowDefinition {
final String keyString;
@@ -188,6 +186,13 @@ class TxDetailRowDefinition {
},
applicable: (vm) => vm.wallet.type == WalletType.monero),
TxDetailRowDefinition(
keyString: "standard_list_item_lightning_preimage",
title: S.current.transaction_preimage,
valueGetter: (vm) => vm.transactionInfo.additionalInfo['preimage'] as String? ?? "",
applicable: (vm) =>
hasLightningPreimage(vm.transactionInfo) && isLightning(vm.transactionInfo),
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_confirmed_key",
@@ -316,10 +321,9 @@ abstract class TransactionDetailsViewModelBase with Store {
TransactionPriority? transactionPriority;
CryptoCurrency get transactionAsset {
if (isEVMCompatibleChain(wallet.type)) return evm!.assetOfTransaction(wallet, transactionInfo);
if (isEVMCompatibleChain(wallet.type)) {
return evm!.assetOfTransaction(wallet, transactionInfo);
}
if (isLightning(transactionInfo)) return CryptoCurrency.btcln;
return switch (wallet.type) {
WalletType.solana => solana!.assetOfTransaction(wallet, transactionInfo),
@@ -329,7 +333,6 @@ abstract class TransactionDetailsViewModelBase with Store {
};
}
// TODO integrate these getters with the TransactionInfo object
String get formattedPendingStatus {
switch (wallet.type) {
@@ -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';