mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-22 06:28:39 +02:00
94 files changed, -5926 net lines. No stubs, no generators, no dead enum
values, no orphan switch branches. Removed for principled reasons (we
do not ship these chains) and to make the codebase smaller and friendlier
to contribute to.
What's gone:
- lib/{solana,tron,zano,decred,zcash,haven}/ directories
- lib/exchange/provider/jupiter_exchange_provider.dart (Solana-only)
- lib/entities/haven_seed_store.dart
- WalletType.{solana,tron,zano,decred,zcash,haven} enum values
(HiveField IDs are pinned: Wownero stays at 12, all surviving types
keep their IDs, so existing user-saved wallets deserialize unchanged)
- All ~70 lib/ files that imported the removed chain stubs or branched
on the removed WalletType values
- backupSeeds(Box<HavenSeedStore>) abstract method + Wownero implementation
- Jupiter exchange branches in send/exchange view models
- TronGrid usage settings, Zano alias lookups, Zcash transparent address
handling, Decred default-spv-nodes branches, Haven seed migration
What stays (intentional):
- CryptoCurrency.{xhv,sol,trx,dcr,zano,zec} enum entries in cw_core —
they're still used as exchange/swap currency identifiers in
address_validator.dart and elsewhere. The wallet types are gone but
the *currencies* are still tradeable through Trocador.
- cw_core/spl_token.dart and cw_core/tron_token.dart Hive adapters in
main.dart — the tokens still register as Hive types for backwards-
compat with any old box data.
- lib/buy/{moonpay,robinhood,onramper,dfx,kryptonim,meld}/ provider
source files (just stripped of solana/tron/etc references; providers
themselves remain dormant per provider_types.dart).
Pinned-ID note: WalletType enum now has gaps in @HiveField IDs (4, 10,
11, 13, 14, 18 removed). This is deliberate — re-using a freed ID later
would mis-deserialize old hive boxes. Don't renumber.
Bundled config / CI hasn't changed; configure.dart was already free of
these chains in commit 4eb7db0b7.
528 lines
17 KiB
Dart
528 lines
17 KiB
Dart
import 'package:hash_wallet/core/address_validator.dart';
|
|
import 'package:hash_wallet/core/yat_service.dart';
|
|
import 'package:hash_wallet/entities/emoji_string_extension.dart';
|
|
import 'package:hash_wallet/entities/ens_record.dart';
|
|
import 'package:hash_wallet/entities/lnurlpay_record.dart';
|
|
import 'package:hash_wallet/entities/fio_address_provider.dart';
|
|
import 'package:hash_wallet/entities/openalias_record.dart';
|
|
import 'package:hash_wallet/entities/parsed_address.dart';
|
|
import 'package:hash_wallet/entities/unstoppable_domain_address.dart';
|
|
import 'package:hash_wallet/entities/wellknown_record.dart';
|
|
import 'package:hash_wallet/entities/zano_alias.dart';
|
|
import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
|
import 'package:hash_wallet/mastodon/mastodon_api.dart';
|
|
import 'package:hash_wallet/nostr/nostr_api.dart';
|
|
import 'package:hash_wallet/store/settings_store.dart';
|
|
import 'package:hash_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);
|
|
}
|
|
|
|
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) {
|
|
String? addressFromBio = extractAddressByType(raw: mastodonUser.note, 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) => 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) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|