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
-172
View File
@@ -1,172 +0,0 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:basic_utils/basic_utils.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:dnssec_proof/dnssec_proof.dart';
import 'package:flutter/material.dart';
class Bip353Record {
Bip353Record({
required this.uri,
required this.domain,
});
final String uri;
final String domain;
static const Map<String, String> keyDisplayMap = {
'lno': 'BOLT 12 Offer',
'sp': 'Silent Payment',
'address': 'On-Chain Address',
};
static Future<String?> fetchDnsProof(String bip353Name) async {
if (bip353Name.startsWith('₿')) {
bip353Name = bip353Name.substring(1);
}
final parts = bip353Name.split('@');
if (parts.length != 2) return null;
final userPart = parts[0];
final domainPart = parts[1];
final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart.';
final proof = await Isolate.run(() => DnsProver.getTxtProof(bip353Domain));
return base64.encode(proof);
}
static Future<Map<String, String>?> fetchUriByCryptoCurrency(
String bip353Name, String asset) async {
try {
// 1. Parse the user and domain from "user@domain"
final parts = bip353Name.split('@');
if (parts.length != 2) return null;
String userPart = parts[0];
if (userPart.startsWith('₿')) {
userPart = userPart.substring(1);
}
final domainPart = parts[1];
// 2. Construct the correct subdomain: "user._bitcoin-payment.domain"
final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart';
// 3. Lookup the TXT record with DNSSEC
final txtRecords = await DnsUtils.lookupRecord(
bip353Domain,
RRecordType.TXT,
dnssec: true,
);
final proof = await fetchDnsProof(bip353Name);
if (proof == null) {
throw Exception('DNSSEC proof not found');
}
if (txtRecords == null) return null;
final assetName = CryptoCurrency.fromString(asset).fullName;
if (assetName == null) throw Exception('Invalid asset name');
final formattedAssetName = assetName.toLowerCase().replaceAll(' ', '') + ':';
for (final record in txtRecords) {
final data = record.data.replaceAll('"', '');
if (data.startsWith(formattedAssetName)) {
return _parseAssetUri(data, formattedAssetName);
}
}
} catch (e) {
printV('BIP353Record.fetchBitcoinUri error: $e');
}
return null;
}
static Map<String, String>? _parseAssetUri(String fullUri, String prefix) {
final afterPrefix = fullUri.substring(prefix.length);
if (afterPrefix.isEmpty) return null;
final questionIndex = afterPrefix.indexOf('?');
if (questionIndex == -1) {
return {'address': afterPrefix};
} else {
final addressPart = afterPrefix.substring(0, questionIndex);
final queryPart = afterPrefix.substring(questionIndex + 1);
final result = <String, String>{};
if (addressPart.isNotEmpty) result['address'] = addressPart;
final queryMap = Uri.splitQueryString(queryPart);
result.addAll(queryMap);
return result;
}
}
static Future<String?> pickBip353AddressChoice(
BuildContext context,
String bip353Name,
Map<String, String> addressMap,
) async {
if (addressMap.length == 1) {
return addressMap.values.first;
}
final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap);
return chosenAddress;
}
static Future<String?> _showAddressChoiceDialog(
BuildContext context,
String bip353Name,
Map<String, String> addressMap,
) async {
final entriesList = addressMap.entries.toList();
final List<Map<String, String>> displayItems = entriesList.map((entry) {
final originalKey = entry.key;
final originalValue = entry.value;
final extendedKeyName = keyDisplayMap[originalKey] ?? originalKey;
final truncatedValue = _truncate(originalValue, front: 6, back: 6);
return {
'displayKey': extendedKeyName,
'displayValue': truncatedValue,
'originalKey': originalKey,
'originalValue': originalValue,
};
}).toList();
String? selectedOriginalValue;
if (context.mounted) {
await showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithPickerOption(
alertTitle: S.of(context).multiple_addresses_detected + '\n$bip353Name',
alertTitleTextSize: 14,
alertSubtitle: S.of(context).please_choose_one + ':',
options: displayItems,
onOptionSelected: (Map<String, String> chosenItem) {
selectedOriginalValue = chosenItem['originalValue'];
},
alertBarrierDismissible: true,
);
},
);
}
return selectedOriginalValue;
}
static String _truncate(String value, {int front = 6, int back = 6}) {
if (value.length <= front + back) return value;
final start = value.substring(0, front);
final end = value.substring(value.length - back);
return '$start...$end';
}
}
-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;
}
}
-76
View File
@@ -1,76 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/lnurl.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class LNUrlPayRecord {
LNUrlPayRecord({
required this.address,
required this.name,
});
final String name;
final String address;
static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency,
{bool allowRethrow = false}) async {
if (![CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return null;
// split the string by the @ symbol:
try {
final List<String> splitStrs = username.split("@");
String name = splitStrs.first.toLowerCase();
final String domain = splitStrs.last;
if (splitStrs.length == 3) {
// for username like @alice@domain.org instead of alice@domain.org
name = splitStrs[1];
}
if (name.isEmpty) {
name = "_";
}
final expectedUrl = "https://$domain/.well-known/lnurlp/$name";
final response = await ProxyWrapper().get(
clearnetUri: Uri.parse(expectedUrl),
headers: <String, String>{"Accept": "application/json"},
);
if (response.statusCode == 200) {
return LNURL.encode(expectedUrl);
}
} catch (e) {
if (allowRethrow) rethrow;
printV("error checking well-known username: $e");
}
return null;
}
static String formatDomainName(String name) {
String formattedName = name;
if (name.contains("@")) {
formattedName = name.replaceAll("@", ".");
}
return formattedName;
}
static Future<LNUrlPayRecord?> fetchAddressAndName({
required String formattedName,
required CryptoCurrency currency,
}) async {
final name = formattedName;
printV("formattedName: $formattedName");
final address = await checkWellKnownUsername(formattedName, currency);
if (address == null) {
return null;
}
return LNUrlPayRecord(address: address, name: name);
}
}
-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;
}
}
-75
View File
@@ -1,75 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class ZcashNamesRecord {
static final Uri _mainnetEndpoint = Uri.parse('https://main.zcashnames.com');
static final RegExp _nameRegExp = RegExp(r'^[A-Za-z0-9_-]{1,63}$');
static Future<String?> fetchZcashNamesAddress(String input) async {
final name = _extractName(input);
if (name == null) return null;
try {
final response = await ProxyWrapper().post(
clearnetUri: _mainnetEndpoint,
headers: const {'Content-Type': 'application/json'},
body: jsonEncode({
'jsonrpc': '2.0',
'id': 1,
'method': 'resolve',
'params': {'query': name},
}),
);
if (response.statusCode != 200) {
printV('ZcashNames: non-200 status ${response.statusCode} for $name');
return null;
}
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic>) return null;
if (decoded['error'] != null) return null;
final result = decoded['result'];
if (result is! Map<String, dynamic>) return null;
final address = result['address'];
if (address is! String || address.isEmpty) return null;
if (!_isValidZcashAddress(address)) {
printV('ZcashNames: returned address failed validation for $name');
return null;
}
return address;
} catch (e) {
printV('ZcashNames: lookup failed for $name: $e');
return null;
}
}
static String? _extractName(String input) {
final lower = input.toLowerCase().trim();
String? raw;
if (lower.endsWith('.zcash')) {
raw = lower.substring(0, lower.length - '.zcash'.length);
} else if (lower.endsWith('.zec')) {
raw = lower.substring(0, lower.length - '.zec'.length);
}
if (raw == null || raw.isEmpty) return null;
if (!_nameRegExp.hasMatch(raw)) return null;
return raw;
}
static bool _isValidZcashAddress(String address) {
final pattern =
AddressValidator.getAddressFromStringPattern(CryptoCurrency.zec);
if (pattern == null) return false;
return RegExp('^(?:$pattern)\$').hasMatch(address);
}
}