mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'dev' into cw-1497-transaction-history-improved-layout
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user