mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-29 04:49:51 +00:00
527 lines
16 KiB
Dart
527 lines
16 KiB
Dart
import 'package:cake_wallet/core/address_validator.dart';
|
|
import 'package:cake_wallet/core/yat_service.dart';
|
|
import 'package:cake_wallet/entities/ens_record.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/emoji_string_extension.dart';
|
|
import 'package:cake_wallet/entities/wellknown_record.dart';
|
|
import 'package:cake_wallet/entities/zano_alias.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/wallet_base.dart';
|
|
import 'package:cw_core/wallet_type.dart';
|
|
import 'package:cake_wallet/entities/fio_address_provider.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
|
|
import 'bip_353_record.dart';
|
|
|
|
class AddressResolverService {
|
|
AddressResolverService({required this.yatService, required this.settingsStore}) {
|
|
_buildLookupTable();
|
|
}
|
|
|
|
final YatService yatService;
|
|
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",
|
|
"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"
|
|
];
|
|
|
|
late final List<LookupEntry> _lookupTable;
|
|
|
|
void _buildLookupTable() {
|
|
_lookupTable = [
|
|
LookupEntry(
|
|
source: AddressSource.twitter,
|
|
currencies: [CryptoCurrency.xmr, CryptoCurrency.btc],
|
|
applies: (q) => settingsStore.lookupsTwitter && q.startsWith('@'),
|
|
// x handle example: @username
|
|
run: _lookupTwitter,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.zanoAlias,
|
|
currencies: [CryptoCurrency.zano],
|
|
applies: (q) => settingsStore.lookupsZanoAlias && q.startsWith('@'),
|
|
// zano handle example: @username
|
|
run: _lookupZano,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.mastodon,
|
|
currencies: [CryptoCurrency.btc],
|
|
applies: (q) =>
|
|
settingsStore.lookupsMastodon &&
|
|
q.startsWith('@') &&
|
|
q.contains('@', 1) &&
|
|
q.contains('.', 1),
|
|
// Mastodon handle example: @username@hostname.xxx
|
|
run: _lookupMastodon,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.wellKnown,
|
|
currencies: [CryptoCurrency.btc],
|
|
applies: (q) => settingsStore.lookupsWellKnown && q.contains('.') && q.contains('@'),
|
|
// .well-known handle example:
|
|
run: _lookupWellKnown,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.fio,
|
|
currencies: [CryptoCurrency.btc],
|
|
applies: (q) => !q.startsWith('@') && q.contains('@') && !q.contains('.'),
|
|
// TODO: Add condition for FIO lookups
|
|
// FIO handle example: username@domain
|
|
run: _lookupFio,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.yatRecord,
|
|
currencies: [CryptoCurrency.btc],
|
|
applies: (q) => settingsStore.lookupsYatService && q.hasOnlyEmojis,
|
|
// Yat handle example: 🐶🐾
|
|
run: _lookupYatService,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.thorChain,
|
|
currencies: [CryptoCurrency.rune],
|
|
applies: (q) => true,
|
|
// ThorChain handles can be any string //TODO: Add condition for ThorChain lookups
|
|
run: _lookupThorChain,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.unstoppableDomains,
|
|
currencies: [CryptoCurrency.btc],
|
|
applies: (q) {
|
|
if (settingsStore.lookupsUnstoppableDomains) return false;
|
|
|
|
// Unstoppable Domains handle example: name.crypto
|
|
final formattedName = OpenaliasRecord.formatDomainName(q);
|
|
final domainParts = formattedName.split('.');
|
|
final name = domainParts.last;
|
|
return domainParts.length > 1 &&
|
|
domainParts.first.isNotEmpty &&
|
|
name.isNotEmpty &&
|
|
unstoppableDomains.any((domain) => name.trim() == domain);
|
|
},
|
|
run: _lookupsUnstoppableDomains,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.bip353,
|
|
currencies: [CryptoCurrency.btc, CryptoCurrency.xmr],
|
|
applies: (q) => true, //TODO: Add condition for BIP-353 lookups
|
|
run: _lookupsBip353,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.ens,
|
|
currencies: [CryptoCurrency.eth],
|
|
applies: (q) => settingsStore.lookupsENS && q.endsWith('.eth'),
|
|
// ENS handle example: name.eth
|
|
run: _lookupEns,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.openAlias,
|
|
currencies: [CryptoCurrency.btc],
|
|
applies: (q) {
|
|
if (settingsStore.lookupsOpenAlias) return false;
|
|
// OpenAlias handle example:
|
|
final formattedName = OpenaliasRecord.formatDomainName(q);
|
|
return formattedName.contains(".");
|
|
},
|
|
run: _lookupsOpenAlias,
|
|
),
|
|
LookupEntry(
|
|
source: AddressSource.nostr,
|
|
currencies: [CryptoCurrency.btc],
|
|
applies: (q) => isEmailFormat(q),
|
|
// Nostr handle example: name@domain //TODO: Add condition for Nostr lookups
|
|
run: _lookupsNostr,
|
|
),
|
|
];
|
|
}
|
|
|
|
static String? extractAddressByType({required String raw, required CryptoCurrency type}) {
|
|
final addressPattern = AddressValidator.getAddressFromStringPattern(type);
|
|
|
|
if (addressPattern == null) {
|
|
throw Exception('Unexpected token: $type for getAddressFromStringPattern');
|
|
}
|
|
|
|
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 '';
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
Future<List<ParsedAddress>> resolve(
|
|
{required String query, required WalletBase wallet, CryptoCurrency? currency}) async {
|
|
final List<ParsedAddress> out = [];
|
|
final List<Future<void>> tasks = [];
|
|
|
|
for (final entry in _lookupTable) {
|
|
if (!entry.applies(query)) continue;
|
|
|
|
final Iterable<CryptoCurrency> coins = currency == null
|
|
? entry.currencies
|
|
: (entry.currencies.contains(currency) ? [currency] : const <CryptoCurrency>[]);
|
|
|
|
for (final cur in coins) {
|
|
tasks.add(entry.run(query, cur, wallet).then((res) {
|
|
if (res != null) out.add(res);
|
|
}));
|
|
}
|
|
}
|
|
|
|
await Future.wait(tasks);
|
|
|
|
if (out.isEmpty) out.add(ParsedAddress(addresses: [query]));
|
|
return out;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupTwitter(
|
|
String text, CryptoCurrency currency, WalletBase wallet) async {
|
|
final formattedName = text.substring(1);
|
|
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
|
|
final addressFromBio = extractAddressByType(
|
|
raw: twitterUser.description,
|
|
type: CryptoCurrency.fromString(currency.title, walletCurrency: wallet.currency));
|
|
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
|
return ParsedAddress.fetchTwitterAddress(
|
|
address: addressFromBio,
|
|
name: text,
|
|
profileImageUrl: twitterUser.profileImageUrl,
|
|
profileName: twitterUser.name);
|
|
}
|
|
|
|
final pinnedTweet = twitterUser.pinnedTweet?.text;
|
|
if (pinnedTweet != null) {
|
|
final addressFromPinnedTweet = extractAddressByType(
|
|
raw: pinnedTweet,
|
|
type: CryptoCurrency.fromString(currency.title, walletCurrency: wallet.currency));
|
|
if (addressFromPinnedTweet != null) {
|
|
return ParsedAddress.fetchTwitterAddress(
|
|
address: addressFromPinnedTweet,
|
|
name: text,
|
|
profileImageUrl: twitterUser.profileImageUrl,
|
|
profileName: twitterUser.name);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupZano(String text, CryptoCurrency currency, WalletBase _) async {
|
|
final formattedName = text.substring(1);
|
|
final zanoAddress = await ZanoAlias.fetchZanoAliasAddress(formattedName);
|
|
if (zanoAddress != null && zanoAddress.isNotEmpty) {
|
|
return ParsedAddress.zanoAddress(
|
|
address: zanoAddress,
|
|
name: text,
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupMastodon(String text, CryptoCurrency currency, WalletBase _) async {
|
|
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) {
|
|
String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: currency);
|
|
|
|
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) => item.content).join('\n');
|
|
String? addressFromPinnedPost =
|
|
extractAddressByType(raw: userPinnedPostsText, type: currency);
|
|
|
|
if (addressFromPinnedPost != null && addressFromPinnedPost.isNotEmpty) {
|
|
return ParsedAddress.fetchMastodonAddress(
|
|
address: addressFromPinnedPost,
|
|
name: text,
|
|
profileImageUrl: mastodonUser.profileImageUrl,
|
|
profileName: mastodonUser.username);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupWellKnown(
|
|
String text, CryptoCurrency currency, WalletBase _) async {
|
|
final record =
|
|
await WellKnownRecord.fetchAddressAndName(formattedName: text, currency: currency);
|
|
if (record != null) {
|
|
return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupFio(String text, CryptoCurrency currency, WalletBase _) async {
|
|
final bool isFioRegistered = await FioAddressProvider.checkAvail(text);
|
|
if (isFioRegistered) {
|
|
final address = await FioAddressProvider.getPubAddress(text, currency.title);
|
|
return ParsedAddress.fetchFioAddress(address: address, name: text);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupYatService(
|
|
String text, CryptoCurrency currency, WalletBase _) async {
|
|
final addresses = await yatService.fetchYatAddress(text, currency.title);
|
|
return ParsedAddress.fetchEmojiAddress(addresses: addresses, name: text);
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupThorChain(
|
|
String text, CryptoCurrency currency, WalletBase _) async {
|
|
final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text);
|
|
if (thorChainAddress != null && thorChainAddress.isNotEmpty) {
|
|
String? address = thorChainAddress[currency.title] ??
|
|
(currency.title == 'RUNE' ? thorChainAddress['THOR'] : null);
|
|
if (address != null) {
|
|
return ParsedAddress.thorChainAddress(address: address, name: text);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupsUnstoppableDomains(
|
|
String text, CryptoCurrency currency, WalletBase _) async {
|
|
final address = await fetchUnstoppableDomainAddress(text, currency.title);
|
|
if (address.isNotEmpty) {
|
|
return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupsBip353(String text, CryptoCurrency currency, WalletBase _) async {
|
|
final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, currency.title);
|
|
|
|
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
|
|
final chosenAddress =
|
|
await Bip353Record.pickBip353AddressChoice(text, bip353AddressMap); //TODO fix context
|
|
if (chosenAddress != null) {
|
|
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupEns(String text, CryptoCurrency currency, WalletBase wallet) async {
|
|
final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet);
|
|
if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") {
|
|
return ParsedAddress.fetchEnsAddress(name: text, address: address);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupsOpenAlias(
|
|
String text, CryptoCurrency currency, WalletBase _) async {
|
|
final formattedName = OpenaliasRecord.formatDomainName(text);
|
|
final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName);
|
|
|
|
if (txtRecord != null) {
|
|
final record = await OpenaliasRecord.fetchAddressAndName(
|
|
formattedName: formattedName, ticker: currency.title.toLowerCase(), txtRecord: txtRecord);
|
|
return ParsedAddress.fetchOpenAliasAddress(record: record, name: text);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<ParsedAddress?> _lookupsNostr(String text, CryptoCurrency currency, WalletBase _) async {
|
|
//TODO implement Nostr lookup logic
|
|
// 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);
|
|
// }
|
|
// }
|
|
// }
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class LookupEntry {
|
|
const LookupEntry({
|
|
required this.source,
|
|
required this.currencies,
|
|
required this.applies,
|
|
required this.run,
|
|
});
|
|
|
|
final AddressSource source;
|
|
final List<CryptoCurrency> currencies;
|
|
final bool Function(String query) applies;
|
|
final Future<ParsedAddress?> Function(String query, CryptoCurrency currenc, WalletBase walle) run;
|
|
}
|