mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-21 06:18:42 +02:00
* feat(zcash): initial commit - initial work on cw_zcash - use single .db for everything zcash to speed up sync and scanning - implement silent autoshielding of non-orchard funds - use FFI for everything to fetch the data - add support for passphrase by adding extra word to seed phrase - enforce lints on CI for cw_zcash directory - use correct zcash name, and call transparent zcash tZEC - minor cleanups outsite of cw_zcash - gitmodules, gitignore - db debug - fix broken addressPageType in WalletInfo - enable debug options in kDebugMode and kProfileMode * feat: T address rotation (and cache) wip: minor fixes * wip: shield tx list * fix fiat amounts fix derrivation of rotation account addresses screen for zcash block height fix T address shields improvements Amount in disposable T shields UI improvements show pending outgoing tx in tx history * fix null check on passphrase page fix tx showing as pending rename auto shield txs fixed send all to include fee use rotating address in addressForExchange wrap all calls of WarpApi.getLatestHeight in try catch to prevent leaking errors other minor fixes from notion * wallet group debug cache sent tx address wallet T address rotation fixes automatically pick the fee for auto-shield * fix T address cache deserialization fix autoshield icon fix autoshield text getCachedDestinationAddress fix send address cache * address comments from review * wip: iOS fix * fix swap * Allow cakepay payment from zcash wallet * fix: ios (I'm stupid) fix: reorder addresses to make orchard default fix: send all balance * 5.6.6 bump fix: ios apple review add transport error to ignored exceptions * fix: leaving dust behind when sweeping all * Change wallets order * fix: reflect balance of pending out txs fix: restore height support * linux support (disabled) * fix: date picker for zcash on restore screen fix: build script for linux match WarpApi.warpSync args with upstream * fix: linter script * ensure all exchange providers support ZEC (#2802) * ensure all exchange providers support ZEC * remove zaddr and tzec currencies and refactor providers * Update lib/exchange/provider/near_Intents_exchange_provider.dart * add back old zcash currencies for backward compatibility, thanks Czarek * minor fix [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update android/app/src/main/AndroidManifestBase.xml [skip ci] * Update ios/Runner/InfoBase.plist [skip ci] * fix: remove unused code fix: macos fix: disable on linux and macos * update build numbers [skip ci] * Add zcash to birdpay --------- Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net> Co-authored-by: Serhii <borodenko.sv@gmail.com>
483 lines
16 KiB
Dart
483 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/emoji_string_extension.dart';
|
|
import 'package:cake_wallet/entities/ens_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/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: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 {
|
|
// 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 (!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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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]);
|
|
}
|
|
}
|