mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Address resolver refactoring (#3291)
* restructure address resolver sources * refactor address resolver into lookup service * refactor address resolver to provider pattern * use addressSource to drive providers & settings * error handling * add address source icons * Update lib/core/address_validator.dart [skip ci] Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * review fixes * add validations to extractParsedAddress * skip address resolver for valid addresses * minor fix [skip ci] * fix formattedName x[skip ci] Co-authored-by: malik1004x <malikowskirobert@gmail.com> * fix formatted name zano [skip ci] Co-authored-by: malik1004x <malikowskirobert@gmail.com> * address PR review comments * add icons for new address source resolvers * confirm parsed addresses in contacts * minor fixes * LNUrl for BTCLN only and contact name handling --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> Co-authored-by: malik1004x <malikowskirobert@gmail.com>
This commit is contained in:
co-authored by
Omar Hatem
malik1004x
parent
00b7d7ce0d
commit
7928c912e8
@@ -0,0 +1,22 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
abstract class AddressLookupProvider {
|
||||
|
||||
AddressSource get source;
|
||||
|
||||
List<CryptoCurrency> get supportedCurrencies;
|
||||
|
||||
bool isEnabled(SettingsStore settingsStore);
|
||||
|
||||
bool canHandle(String query);
|
||||
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/bip_353/bip_353_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/ens/ens_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/fio/fio_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurl_pay_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/nostr/nostr_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/openalias/openalias_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/thorchain/thorchain_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/twitter/twitter_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/wellknown/wellknown_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/yat/yat_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/zano/zano_alias_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/zcash/zcash_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/zcash/zcash_me_address_provider.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class AddressResolverService {
|
||||
AddressResolverService({required this.settingsStore});
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
// Sources available for address resolution and lookup settings.
|
||||
static const supportedSources = [
|
||||
AddressSource.lnurlPay,
|
||||
AddressSource.twitter,
|
||||
AddressSource.mastodon,
|
||||
AddressSource.yatRecord,
|
||||
AddressSource.unstoppableDomains,
|
||||
AddressSource.openAlias,
|
||||
AddressSource.ens,
|
||||
AddressSource.zcashName,
|
||||
AddressSource.zcashAddress,
|
||||
AddressSource.wellKnown,
|
||||
AddressSource.zanoAlias,
|
||||
AddressSource.bip353,
|
||||
AddressSource.fio,
|
||||
AddressSource.thorChain,
|
||||
AddressSource.nostr,
|
||||
];
|
||||
|
||||
List<AddressLookupProvider> _buildProviders() {
|
||||
final allProviders = <AddressLookupProvider>[
|
||||
LNUrlPayAddressProvider(),
|
||||
TwitterAddressProvider(),
|
||||
MastodonAddressProvider(),
|
||||
YatAddressProvider(),
|
||||
UnstoppableAddressProvider(),
|
||||
OpenaliasAddressProvider(),
|
||||
EnsAddressProvider(),
|
||||
ZcashNameAddressProvider(),
|
||||
ZcashMeAddressProvider(),
|
||||
WellKnownAddressProvider(),
|
||||
ZanoAliasAddressProvider(),
|
||||
Bip353AddressProvider(),
|
||||
FioAddressProvider(),
|
||||
ThorchainAddressProvider(),
|
||||
NostrAddressProvider(),
|
||||
];
|
||||
|
||||
return allProviders.where((provider) => supportedSources.contains(provider.source)).toList();
|
||||
}
|
||||
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required WalletBase wallet,
|
||||
required CryptoCurrency currency,
|
||||
}) async {
|
||||
try {
|
||||
final providers = _buildProviders();
|
||||
// return first entry if currency is specified and provider supports it
|
||||
if (currency != null) {
|
||||
for (final provider in providers) {
|
||||
final cur = currency.title;
|
||||
final src = provider.source.label;
|
||||
|
||||
printV('[address resolver service] checking $src - $cur - $query');
|
||||
if (!provider.isEnabled(settingsStore)) {
|
||||
printV('[address resolver service] ...skipping $src (disabled)');
|
||||
continue;
|
||||
}
|
||||
if (!provider.canHandle(query)) {
|
||||
printV('[address resolver service] ...skipping $src (cannot handle)');
|
||||
continue;
|
||||
}
|
||||
if (!provider.supportedCurrencies.contains(currency)) {
|
||||
printV('[address resolver service] ...skipping $src (unsupported currency)');
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
printV('[address resolver service] ...resolving $src - $cur - $query');
|
||||
final results = await provider.resolve(
|
||||
query: query,
|
||||
currencies: [currency],
|
||||
wallet: wallet,
|
||||
);
|
||||
|
||||
if (results.isNotEmpty) {
|
||||
final parsedAddress = results.first.parsedAddressByCurrencyMap[currency];
|
||||
final parsedAddressStr = parsedAddress == null || parsedAddress.isEmpty ? 'N/A' : parsedAddress;
|
||||
printV('[address resolver service] resolved $src - $cur - $parsedAddressStr');
|
||||
return [results.first];
|
||||
}
|
||||
} catch (e) {
|
||||
printV('[address resolver service] Error resolving $src: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return []; // TODO: Implement multi-currency resolution if no specific currency is provided.
|
||||
|
||||
final tasks = <Future<List<ParsedAddress>>>[];
|
||||
|
||||
for (final provider in providers) {
|
||||
if (!provider.isEnabled(settingsStore)) continue;
|
||||
if (!provider.canHandle(query)) continue;
|
||||
|
||||
tasks.add(
|
||||
provider.resolve(
|
||||
query: query,
|
||||
currencies: provider.supportedCurrencies,
|
||||
wallet: wallet,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final results = await Future.wait(tasks);
|
||||
return results.expand((items) => items).toList();
|
||||
} catch (e) {
|
||||
printV('Error resolving address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
||||
class AddressResolverUtils {
|
||||
static Future<Map<CryptoCurrency, String>> resolveUnstoppableDomainFromText({
|
||||
required String raw,
|
||||
required List<CryptoCurrency> currencies,
|
||||
}) async {
|
||||
final domain = extractUnstoppableDomain(raw);
|
||||
if (domain.isEmpty) return {};
|
||||
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
for (final currency in currencies) {
|
||||
final address = await UnstoppableAddressProvider.fetchUnstoppableDomainAddress(domain, currency.title);
|
||||
if (address.isNotEmpty) {
|
||||
result[currency] = address;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static String extractUnstoppableDomain(String raw) {
|
||||
// sort by length to avoid matching shorter tld instead of longer
|
||||
final domains = List<String>.from(UnstoppableAddressProvider.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 '';
|
||||
}
|
||||
|
||||
static Map<CryptoCurrency, String> extractAddressesFromText({
|
||||
required String raw,
|
||||
required List<CryptoCurrency> currencies,
|
||||
bool requireSurroundingWhitespaces = true,
|
||||
}) {
|
||||
final result = <CryptoCurrency, String>{};
|
||||
var text = raw;
|
||||
|
||||
for (final currency in currencies) {
|
||||
final address = extractAddressByType(
|
||||
raw: text,
|
||||
type: currency,
|
||||
requireSurroundingWhitespaces: requireSurroundingWhitespaces,
|
||||
);
|
||||
|
||||
if (address != null && address.isNotEmpty) {
|
||||
result[currency] = address;
|
||||
text = text.replaceFirst(address, '');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static String? extractAddressByType({
|
||||
required String raw,
|
||||
required CryptoCurrency type,
|
||||
bool requireSurroundingWhitespaces = true,
|
||||
}) {
|
||||
var addressPattern = AddressValidator.getAddressFromStringPattern(type);
|
||||
if (addressPattern == null) {
|
||||
printV('Unknown pattern for $type');
|
||||
return null;
|
||||
}
|
||||
if (requireSurroundingWhitespaces) addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX";
|
||||
final text = stripHtmlTags(raw);
|
||||
final match = RegExp(addressPattern, multiLine: true, caseSensitive: false).firstMatch(text);
|
||||
if (match == null) return null;
|
||||
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 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);
|
||||
}
|
||||
|
||||
static String stripHtmlTags(String value) {
|
||||
return value
|
||||
.replaceAll(RegExp(r'[\u2028\u2029]'), '\n')
|
||||
.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
|
||||
.replaceAll(RegExp(r'</p\s*>', caseSensitive: false), '\n')
|
||||
.replaceAll(RegExp(r'<[^>]*>'), ' ')
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
enum AddressSource {
|
||||
twitter(label: 'X', iconPath: 'assets/new-ui/address_sources/x.svg', alias: '@username'),
|
||||
unstoppableDomains(
|
||||
label: 'Unstoppable Domains',
|
||||
iconPath: 'assets/new-ui/address_sources/unstoppable.svg',
|
||||
alias: 'domain.tld'),
|
||||
openAlias(
|
||||
label: 'OpenAlias',
|
||||
iconPath: 'assets/new-ui/address_sources/openalias.svg',
|
||||
alias: 'name.domain.tld'),
|
||||
yatRecord(label: 'Yat', iconPath: 'assets/new-ui/address_sources/yat.svg', alias: '🎂🎂🎂'),
|
||||
fio(label: 'FIO', iconPath: 'assets/new-ui/address_sources/fio.svg', alias: 'user@domain'),
|
||||
ens(
|
||||
label: 'Ethereum Name Service',
|
||||
iconPath: 'assets/new-ui/address_sources/ens.svg',
|
||||
alias: 'domain.eth'),
|
||||
mastodon(
|
||||
label: 'Mastodon',
|
||||
iconPath: 'assets/new-ui/address_sources/mastodon.svg',
|
||||
alias: '@username@domain.tld'),
|
||||
nostr(
|
||||
label: 'Nostr',
|
||||
iconPath: 'assets/new-ui/address_sources/nostr.svg',
|
||||
alias: 'user@domain.tld'),
|
||||
thorChain(
|
||||
label: 'ThorChain',
|
||||
iconPath: 'assets/new-ui/address_sources/icon-thorchain.svg',
|
||||
alias: 'name'),
|
||||
wellKnown(
|
||||
label: '.wellknown',
|
||||
iconPath: 'assets/new-ui/address_sources/wellknown.svg',
|
||||
alias: 'domain.tld'),
|
||||
zanoAlias(
|
||||
label: 'Zano Alias', iconPath: 'assets/new-ui/address_sources/zano.svg', alias: '@alias'),
|
||||
bip353(
|
||||
label: 'BIP353',
|
||||
iconPath: 'assets/new-ui/address_sources/bip353.svg',
|
||||
alias: 'user@domain.com'),
|
||||
zcashAddress(
|
||||
label: 'Zcash.me',
|
||||
iconPath: 'assets/new-ui/address_sources/icon-zcash-me.svg',
|
||||
alias: 'zcash.me/username'),
|
||||
zcashName(
|
||||
label: 'Zcash Names',
|
||||
iconPath: 'assets/new-ui/address_sources/icon-zcashnames.svg',
|
||||
alias: 'name.zec'),
|
||||
lnurlPay(
|
||||
label: 'LNURL Pay',
|
||||
iconPath: 'assets/new-ui/address_sources/icon-lnurl-pay.svg',
|
||||
alias: 'user@domain.com'),
|
||||
contact(label: 'Contact', iconPath: ''),
|
||||
notParsed(label: 'Unknown', iconPath: ''),
|
||||
;
|
||||
|
||||
const AddressSource({
|
||||
required this.label,
|
||||
required this.iconPath,
|
||||
this.alias = '',
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String iconPath;
|
||||
final String alias;
|
||||
}
|
||||
|
||||
extension AddressSourceIndex on AddressSource {
|
||||
int get raw => index;
|
||||
|
||||
static AddressSource fromRaw(int raw) =>
|
||||
AddressSource.values[raw.clamp(0, AddressSource.values.length - 1)];
|
||||
}
|
||||
|
||||
extension AddressSourceNameParser on AddressSource {
|
||||
static AddressSource fromLabel(String? text) {
|
||||
if (text == null || text.trim().isEmpty) {
|
||||
return AddressSource.notParsed;
|
||||
}
|
||||
final needle = text.trim().toLowerCase();
|
||||
return AddressSource.values.firstWhere(
|
||||
(src) => src.label.toLowerCase() == needle,
|
||||
orElse: () => AddressSource.notParsed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/bip_353/bip_353_record.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class Bip353AddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.bip353;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.btc];
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsBip353;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) {
|
||||
final value = query.trim();
|
||||
return value.contains('@') && value.contains('.') && !value.startsWith('@');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final result = <CryptoCurrency, String>{};
|
||||
String? dnsProof;
|
||||
|
||||
for (final cur in currencies) {
|
||||
final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(query, cur.title);
|
||||
if (bip353AddressMap == null || bip353AddressMap.isEmpty) continue;
|
||||
|
||||
final spAddress = bip353AddressMap['sp'];
|
||||
final address = bip353AddressMap['address'];
|
||||
final chosenAddress = spAddress?.isNotEmpty == true ? spAddress : address;
|
||||
|
||||
if (chosenAddress != null && chosenAddress.isNotEmpty) {
|
||||
result[cur] = chosenAddress;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
try {
|
||||
dnsProof = await Bip353Record.fetchDnsProof(query);
|
||||
} catch (e) {
|
||||
printV('Bip353Record.fetchDnsProof error: $e');
|
||||
}
|
||||
|
||||
return [ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.bip353,
|
||||
handle: query,
|
||||
bip353DnsProof: dnsProof ?? '',
|
||||
)];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving BIP353 address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:basic_utils/basic_utils.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:dnssec_proof/dnssec_proof.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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/ens/ens_record.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class EnsAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.ens;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies =>
|
||||
[CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth];
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsENS;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) => query.endsWith('.eth'); // ENS handle example: name.eth
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
|
||||
for (final cur in currencies) {
|
||||
final address = await EnsRecord.fetchEnsAddress(query, cur, wallet: wallet);
|
||||
if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") {
|
||||
result[cur] = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.ens,
|
||||
handle: query,
|
||||
)
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cw_core/crypto_currency.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,
|
||||
CryptoCurrency cur, {
|
||||
WalletBase? wallet,
|
||||
}) async {
|
||||
Web3Client? _client;
|
||||
|
||||
if (wallet?.type == WalletType.ethereum || wallet?.type == WalletType.polygon) {
|
||||
_client = evm!.getWeb3Client(wallet!);
|
||||
}
|
||||
|
||||
_client ??= Web3Client(
|
||||
'https://ethereum-rpc.publicnode.com',
|
||||
ProxyWrapper().getHttpIOClient(),
|
||||
);
|
||||
|
||||
final ens = Ens(client: _client);
|
||||
final coinType = getEnsCoinType(cur);
|
||||
|
||||
if (coinType == null) {
|
||||
printV('Unsupported currency for ENS: $cur');
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
if (coinType == CoinType.ETH || coinType == CoinType.MATIC) {
|
||||
return (await ens.withName(name).getAddress()).hex;
|
||||
} else {
|
||||
return await ens.withName(name).getCoinAddress(coinType);
|
||||
}
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static CoinType? getEnsCoinType(CryptoCurrency cur) => switch (cur) {
|
||||
CryptoCurrency.xmr => CoinType.XMR,
|
||||
CryptoCurrency.btc => CoinType.BTC,
|
||||
CryptoCurrency.ltc => CoinType.LTC,
|
||||
CryptoCurrency.eth => CoinType.ETH,
|
||||
CryptoCurrency.matic => CoinType.MATIC,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/store/settings_store.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';
|
||||
|
||||
class FioAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.fio;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
|
||||
|
||||
@override
|
||||
bool canHandle(String q) =>
|
||||
!q.startsWith('@') &&
|
||||
q.contains('@') &&
|
||||
!q.contains('.'); // FIO handle example: username@domain
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsFio;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
final Map<CryptoCurrency, String> result = {};
|
||||
try {
|
||||
final bool isFioRegistered = await FioAddressProvider.checkAvail(query);
|
||||
if (!isFioRegistered) return [];
|
||||
|
||||
for (final cur in currencies) {
|
||||
final address = await FioAddressProvider.getPubAddress(query, cur.title);
|
||||
if (address != null && address.isNotEmpty) {
|
||||
result[cur] = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result, addressSource: AddressSource.fio, handle: query)
|
||||
];
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
printV('FioAddressProvider.resolve error: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final error = responseJSON['error'] as String;
|
||||
final message = responseJSON['message'] as String;
|
||||
printV('${error}\n$message');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final String message = responseJSON['message'] as String? ?? 'Unknown error';
|
||||
|
||||
printV('Error fetching public address for token $token: $message');
|
||||
return null;
|
||||
}
|
||||
|
||||
final String pubAddress = responseJSON['public_address'] as String? ?? '';
|
||||
|
||||
if (pubAddress.isNotEmpty) {
|
||||
return pubAddress;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurlpay_record.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/store/settings_store.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';
|
||||
|
||||
class LNUrlPayAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.lnurlPay;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.btcln];
|
||||
|
||||
@override
|
||||
bool canHandle(String q) => q.contains('.') && q.contains('@'); // LNURL-pay handle example: user@domain.com
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsLNUrl;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
if (wallet.type != WalletType.bitcoin) return [];
|
||||
|
||||
final formattedName = query.trim();
|
||||
if (formattedName.isEmpty) return [];
|
||||
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
for (final currency in currencies) {
|
||||
if (!supportedCurrencies.contains(currency)) continue;
|
||||
|
||||
final record = await LNUrlPayRecord.fetchAddressAndName(
|
||||
formattedName: formattedName,
|
||||
currency: currency,
|
||||
);
|
||||
|
||||
if (record != null && record.address.isNotEmpty) {
|
||||
result[currency] = record.address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.lnurlPay,
|
||||
handle: formattedName,
|
||||
profileName: formattedName,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving LNURL-pay address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_api.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class MastodonAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.mastodon;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsMastodon;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) =>
|
||||
query.startsWith('@') &&
|
||||
query.contains('@', 1) &&
|
||||
query.contains('.', 1); // Mastodon handle example: @username@hostname.xxx
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final subText = query.substring(1); // Remove '@' from the beginning of the handle
|
||||
final hostNameIndex = subText.indexOf('@');
|
||||
if (hostNameIndex <= 0 || hostNameIndex == subText.length - 1) return [];
|
||||
|
||||
final userName = subText.substring(0, hostNameIndex);
|
||||
final hostName = subText.substring(hostNameIndex + 1);
|
||||
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
final mastodonUser = await MastodonAPI.lookupUserByUserName(
|
||||
userName: userName,
|
||||
apiHost: hostName,
|
||||
);
|
||||
if (mastodonUser == null) return [];
|
||||
|
||||
final bio = AddressResolverUtils.stripHtmlTags(mastodonUser.note);
|
||||
result.addAll(
|
||||
AddressResolverUtils.extractAddressesFromText(
|
||||
raw: bio,
|
||||
currencies: currencies,
|
||||
),
|
||||
);
|
||||
|
||||
final pinnedPosts = await MastodonAPI.getPinnedPosts(
|
||||
userId: mastodonUser.id,
|
||||
apiHost: hostName,
|
||||
);
|
||||
|
||||
if (pinnedPosts.isNotEmpty) {
|
||||
final pinnedPostsText = pinnedPosts
|
||||
.map((item) => AddressResolverUtils.stripHtmlTags(item.content))
|
||||
.where((text) => text.isNotEmpty)
|
||||
.join('\n');
|
||||
|
||||
result.addAll(
|
||||
AddressResolverUtils.extractAddressesFromText(
|
||||
raw: pinnedPostsText,
|
||||
currencies: currencies,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.mastodon,
|
||||
handle: query,
|
||||
profileImageUrl: mastodonUser.profileImageUrl,
|
||||
profileName: mastodonUser.username,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving Mastodon address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'dart:convert';
|
||||
import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_user.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
||||
class MastodonAPI {
|
||||
static const httpsScheme = 'https';
|
||||
static const userPath = '/api/v1/accounts/lookup';
|
||||
static const statusesPath = '/api/v1/accounts/:id/statuses';
|
||||
|
||||
static Future<MastodonUser?> lookupUserByUserName(
|
||||
{required String userName, required String apiHost}) async {
|
||||
try {
|
||||
final queryParams = {'acct': userName};
|
||||
|
||||
final uri = Uri(
|
||||
scheme: httpsScheme,
|
||||
host: apiHost,
|
||||
path: userPath,
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
if (response.statusCode != 200) return null;
|
||||
|
||||
final Map<String, dynamic> responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
return MastodonUser.fromJson(responseJSON);
|
||||
} catch (e) {
|
||||
printV('Error in lookupUserByUserName: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<PinnedPost>> getPinnedPosts({
|
||||
required String userId,
|
||||
required String apiHost,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = {'pinned': 'true'};
|
||||
|
||||
final uri = Uri(
|
||||
scheme: httpsScheme,
|
||||
host: apiHost,
|
||||
path: statusesPath.replaceAll(':id', userId),
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected HTTP status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final List<dynamic> responseJSON = json.decode(response.body) as List<dynamic>;
|
||||
|
||||
return responseJSON.map((json) => PinnedPost.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e) {
|
||||
printV('Error in getPinnedPosts: $e');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
class MastodonUser {
|
||||
String id;
|
||||
String username;
|
||||
String profileImageUrl;
|
||||
String acct;
|
||||
String note;
|
||||
|
||||
MastodonUser({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.profileImageUrl,
|
||||
required this.acct,
|
||||
required this.note,
|
||||
});
|
||||
|
||||
factory MastodonUser.fromJson(Map<String, dynamic> json) {
|
||||
return MastodonUser(
|
||||
id: json['id'] as String,
|
||||
username: json['username'] as String? ?? '',
|
||||
acct: json['acct'] as String,
|
||||
note: json['note'] as String,
|
||||
profileImageUrl: json['avatar'] as String? ?? ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PinnedPost {
|
||||
final String id;
|
||||
final String content;
|
||||
|
||||
PinnedPost({required this.id, required this.content});
|
||||
|
||||
factory PinnedPost.fromJson(Map<String, dynamic> json) {
|
||||
return PinnedPost(
|
||||
id: json['id'] as String,
|
||||
content: json['content'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/nostr/nostr_api.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class NostrAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.nostr;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
|
||||
|
||||
@override
|
||||
bool canHandle(String q) =>
|
||||
AddressResolverUtils.isEmailFormat(q); // Nostr handle example: username@domain
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsNostr;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final profile = await NostrProfileHandler.queryProfile(query);
|
||||
if (profile == null) return [];
|
||||
|
||||
final data = await NostrProfileHandler.processRelays(profile, query);
|
||||
if (data == null) return [];
|
||||
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
String queryTxt = data.about;
|
||||
|
||||
for (final cur in currencies) {
|
||||
final addr = AddressResolverUtils.extractAddressByType(raw: queryTxt, type: cur);
|
||||
if (addr != null && addr.isNotEmpty) {
|
||||
result[cur] = addr;
|
||||
queryTxt = queryTxt.replaceFirst(addr, '');
|
||||
}
|
||||
}
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.nostr,
|
||||
handle: query,
|
||||
profileImageUrl: data.picture,
|
||||
profileName: data.name,
|
||||
)
|
||||
];
|
||||
} catch (e) {
|
||||
printV('Error looking up Nostr profile: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/core/address_resolver/nostr/nostr_user.dart';
|
||||
import 'package:nostr_tools/nostr_tools.dart';
|
||||
import 'dart:async' show Completer, TimeoutException, runZonedGuarded;
|
||||
|
||||
class NostrProfileHandler {
|
||||
static final relayToDomainMap = {
|
||||
'relay.snort.social': 'snort.social',
|
||||
};
|
||||
|
||||
static final Nip05 _nip05 = Nip05();
|
||||
|
||||
static Future<ProfilePointer?> queryProfile(String nip05Address) async {
|
||||
final profile = await _nip05.queryProfile(nip05Address);
|
||||
if (profile?.pubkey != null && profile?.relays?.isNotEmpty == true) {
|
||||
return profile;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<UserMetadata?> processRelays(
|
||||
ProfilePointer profile,
|
||||
String nip05Address,
|
||||
) async {
|
||||
final userDomain = _extractDomain(nip05Address);
|
||||
const int metaKind = 0;
|
||||
|
||||
// Domain-matched relays first
|
||||
for (final String relayUrl in profile.relays ?? []) {
|
||||
final relayDomain =
|
||||
relayToDomainMap[_getDomainFromRelayUrl(relayUrl)] ?? _getDomainFromRelayUrl(relayUrl);
|
||||
|
||||
if (relayDomain == userDomain) {
|
||||
final data = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaKind]);
|
||||
if (data != null) return data;
|
||||
}
|
||||
}
|
||||
|
||||
// Then try every remaining relay
|
||||
for (final String relayUrl in profile.relays ?? []) {
|
||||
final data = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaKind]);
|
||||
if (data != null) return data;
|
||||
}
|
||||
|
||||
// Nothing found
|
||||
return null;
|
||||
}
|
||||
|
||||
static const Duration _relayTimeout = Duration(seconds: 3);
|
||||
|
||||
static Future<UserMetadata?> _fetchInfoFromRelay(
|
||||
String relayUrl, String userPubKey, List<int> kinds) async {
|
||||
// sanitize so obvious junk (like '#') doesn't reach connect()
|
||||
final clean = _sanitizeRelay(relayUrl);
|
||||
if (clean.isEmpty) return null;
|
||||
|
||||
final result = Completer<UserMetadata?>();
|
||||
|
||||
runZonedGuarded(() async {
|
||||
try {
|
||||
final relay = RelayApi(relayUrl: clean);
|
||||
|
||||
final stream = await relay.connect().timeout(
|
||||
_relayTimeout,
|
||||
onTimeout: () {
|
||||
relay.close();
|
||||
throw TimeoutException('Relay connect timeout');
|
||||
},
|
||||
);
|
||||
|
||||
relay.sub([
|
||||
Filter(kinds: kinds, authors: [userPubKey])
|
||||
]);
|
||||
|
||||
final sub = stream.listen((msg) {
|
||||
if (msg.type == 'EVENT' && !result.isCompleted) {
|
||||
try {
|
||||
final event = msg.message as Event;
|
||||
final jsonMap = json.decode(event.content) as Map<String, dynamic>;
|
||||
result.complete(UserMetadata.fromJson(jsonMap));
|
||||
} catch (_) {
|
||||
if (!result.isCompleted) result.complete(null);
|
||||
}
|
||||
}
|
||||
}, onError: (_) {
|
||||
if (!result.isCompleted) result.complete(null);
|
||||
}, onDone: () {
|
||||
if (!result.isCompleted) result.complete(null);
|
||||
});
|
||||
|
||||
final value = await result.future.timeout(_relayTimeout, onTimeout: () => null);
|
||||
await sub.cancel();
|
||||
relay.close();
|
||||
if (!result.isCompleted) result.complete(value);
|
||||
} catch (_) {
|
||||
if (!result.isCompleted) result.complete(null);
|
||||
}
|
||||
}, (error, stack) {
|
||||
// swallow ALL async errors from the websocket layer (including "was not upgraded to websocket")
|
||||
if (!result.isCompleted) result.complete(null);
|
||||
});
|
||||
|
||||
return result.future;
|
||||
}
|
||||
|
||||
static String _sanitizeRelay(String url) {
|
||||
url = url.replaceFirst(RegExp(r'^https?://'), 'wss://');
|
||||
final uri = Uri.parse(url);
|
||||
return Uri(
|
||||
scheme: uri.scheme.isEmpty ? 'wss' : uri.scheme,
|
||||
host: uri.host,
|
||||
port: uri.hasPort ? uri.port : 443,
|
||||
).toString();
|
||||
}
|
||||
|
||||
static String _extractDomain(String nip05) => nip05.split('@').last;
|
||||
|
||||
static String _getDomainFromRelayUrl(String url) {
|
||||
try {
|
||||
return Uri.parse(url).host;
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
class UserMetadata {
|
||||
final String name;
|
||||
final String lnurl;
|
||||
final String email;
|
||||
final String picture;
|
||||
final String about;
|
||||
final String nip05;
|
||||
final String banner;
|
||||
final String website;
|
||||
|
||||
UserMetadata({
|
||||
required this.name,
|
||||
required this.lnurl,
|
||||
required this.email,
|
||||
required this.picture,
|
||||
required this.about,
|
||||
required this.nip05,
|
||||
required this.banner,
|
||||
required this.website,
|
||||
});
|
||||
|
||||
factory UserMetadata.fromJson(Map<String, dynamic> json) {
|
||||
return UserMetadata(
|
||||
name: json['name'] as String? ?? '',
|
||||
lnurl: json['lud06'] as String? ?? '',
|
||||
email: json['lud16'] as String? ?? '',
|
||||
picture: json['picture'] as String? ?? '',
|
||||
about: json['about'] as String? ?? '',
|
||||
nip05: json['nip05'] as String? ?? '',
|
||||
banner: json['banner'] as String? ?? '',
|
||||
website: json['website'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/openalias/openalias_record.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class OpenaliasAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.openAlias;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.xmr, CryptoCurrency.btc];
|
||||
|
||||
@override
|
||||
bool canHandle(String q) {
|
||||
final formattedName = OpenaliasRecord.formatDomainName(q);
|
||||
return formattedName.contains(".");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsOpenAlias;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final formatted = OpenaliasRecord.formatDomainName(query);
|
||||
|
||||
final txtRecords = await OpenaliasRecord.lookupOpenAliasRecord(formatted);
|
||||
if (txtRecords == null) return [];
|
||||
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
for (final cur in currencies) {
|
||||
final rec = OpenaliasRecord.fetchAddressAndName(
|
||||
formattedName: formatted,
|
||||
ticker: cur.title.toLowerCase(),
|
||||
txtRecord: txtRecords,
|
||||
);
|
||||
|
||||
if (rec.address.isNotEmpty) result[cur] = rec.address;
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.openAlias,
|
||||
handle: query,
|
||||
)
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving OpenAlias address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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,
|
||||
}) {
|
||||
var address = '';
|
||||
var name = formattedName;
|
||||
var note = '';
|
||||
|
||||
final addrRe = RegExp(r'recipient_address=([^;\s]+)', caseSensitive: false);
|
||||
final nameRe = RegExp(r'recipient_name=([^;]+)', caseSensitive: false);
|
||||
final noteRe = RegExp(r'tx_description=([^;]+)', caseSensitive: false);
|
||||
final tag = 'oa1:$ticker';
|
||||
|
||||
for (final rr in txtRecord) {
|
||||
final txt = rr.data.replaceAll('"', '');
|
||||
|
||||
if (!txt.toLowerCase().contains(tag)) continue;
|
||||
|
||||
final addrM = addrRe.firstMatch(txt);
|
||||
if (addrM != null) address = addrM.group(1)!;
|
||||
|
||||
final nameM = nameRe.firstMatch(txt);
|
||||
if (nameM != null) name = nameM.group(1)!.trim();
|
||||
|
||||
final noteM = noteRe.firstMatch(txt);
|
||||
if (noteM != null) note = noteM.group(1)!.trim();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return OpenaliasRecord(address: address, name: name, description: note);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class ParsedAddress {
|
||||
const ParsedAddress({
|
||||
required this.parsedAddressByCurrencyMap,
|
||||
this.addressSource = AddressSource.notParsed,
|
||||
this.handle = '',
|
||||
this.profileImageUrl = '',
|
||||
this.profileName = '',
|
||||
this.description = '',
|
||||
this.bip353DnsProof,
|
||||
});
|
||||
|
||||
final Map<CryptoCurrency, String> parsedAddressByCurrencyMap;
|
||||
final AddressSource addressSource;
|
||||
final String handle;
|
||||
final String profileImageUrl;
|
||||
final String profileName;
|
||||
final String description;
|
||||
final String? bip353DnsProof;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class ThorchainAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.thorChain;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
|
||||
|
||||
@override
|
||||
bool canHandle(String q) => q.isNotEmpty; // can be any string
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsThorChain;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final isNormalAddress = currencies.any((cur) =>
|
||||
AddressResolverUtils.extractAddressByType(raw: query, type: cur)?.isNotEmpty ?? false);
|
||||
if (query.length > 30 || isNormalAddress) return [];
|
||||
|
||||
final map = await ThorChainExchangeProvider.lookupAddressByName(query);
|
||||
if (map == null || map.isEmpty) return [];
|
||||
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
for (final cur in currencies) {
|
||||
final key = cur.title.toUpperCase();
|
||||
final addr = map[key];
|
||||
final runeAddr = cur.title.toUpperCase() == 'RUNE' ? map['THOR'] : null;
|
||||
final resolvedAddress = addr ?? runeAddr;
|
||||
if (resolvedAddress != null && resolvedAddress.isNotEmpty) {
|
||||
if (!result.containsValue(resolvedAddress)) result[cur] = resolvedAddress;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.thorChain,
|
||||
handle: query,
|
||||
)
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/twitter/twitter_api.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class TwitterAddressProvider extends AddressLookupProvider {
|
||||
|
||||
@override
|
||||
AddressSource get source => AddressSource.twitter;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsTwitter;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) =>
|
||||
query.startsWith('@') && !query.substring(1).contains('@'); // x handle example: @username
|
||||
|
||||
/// Resolves a Twitter handle to a list of ParsedAddress objects by extracting potential cryptocurrency addresses
|
||||
/// from the user's, profile.
|
||||
/// fallback to resolving Unstoppable Domains if no addresses are found in each of the Twitter user's bio, location, and pinned tweet.
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
final formattedName = query.replaceFirst("@", "");
|
||||
try {
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
|
||||
|
||||
final bio = (text: twitterUser.description, whitespaces: true);
|
||||
final location = (text: twitterUser.location, whitespaces: false);
|
||||
final pinnedTweet = (text: twitterUser.pinnedTweet?.text ?? '', whitespaces: true);
|
||||
|
||||
final twitterTexts = [
|
||||
bio,
|
||||
location,
|
||||
pinnedTweet,
|
||||
].where((item) => item.text.isNotEmpty);
|
||||
|
||||
for (final item in twitterTexts) {
|
||||
result.addAll(
|
||||
AddressResolverUtils.extractAddressesFromText(
|
||||
raw: item.text,
|
||||
currencies: currencies,
|
||||
requireSurroundingWhitespaces: item.whitespaces,
|
||||
),
|
||||
);
|
||||
|
||||
if (result.isEmpty) {
|
||||
final domainResult = await AddressResolverUtils.resolveUnstoppableDomainFromText(
|
||||
raw: item.text,
|
||||
currencies: currencies,
|
||||
);
|
||||
|
||||
if (domainResult.isNotEmpty) {
|
||||
return [_buildTwitterParsedAddress(query, twitterUser, domainResult)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [_buildTwitterParsedAddress(query, twitterUser, result)];
|
||||
} catch (e) {
|
||||
printV('[address resolver service] Twitter error: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
ParsedAddress _buildTwitterParsedAddress(
|
||||
String query,
|
||||
TwitterUser twitterUser,
|
||||
Map<CryptoCurrency, String> result,
|
||||
) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.twitter,
|
||||
handle: query,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
class TwitterApi {
|
||||
static const twitterBearerToken = secrets.twitterBearerToken;
|
||||
static const httpsScheme = 'https';
|
||||
static const apiHost = 'api.twitter.com';
|
||||
static const userPath = '/2/users/by/username/';
|
||||
|
||||
static Future<TwitterUser> lookupUserByName({required String userName}) async {
|
||||
final queryParams = {
|
||||
'user.fields': 'description,profile_image_url,location',
|
||||
'expansions': 'pinned_tweet_id',
|
||||
'tweet.fields': 'note_tweet'
|
||||
};
|
||||
final headers = {'authorization': 'Bearer $twitterBearerToken'};
|
||||
final uri = Uri(
|
||||
scheme: httpsScheme,
|
||||
host: apiHost,
|
||||
path: userPath + userName,
|
||||
queryParameters: queryParams);
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers).catchError((error) {
|
||||
throw Exception('HTTP request failed: $error');
|
||||
});
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
|
||||
final Map<String, dynamic> responseJSON = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
if (responseJSON['errors'] != null &&
|
||||
!responseJSON['errors'][0]['detail']
|
||||
.toString()
|
||||
.contains("Could not find tweet with pinned_tweet_id")) {
|
||||
throw Exception(responseJSON['errors'][0]['detail']);
|
||||
}
|
||||
|
||||
return TwitterUser.fromJson(responseJSON, _getPinnedTweet(responseJSON));
|
||||
}
|
||||
|
||||
static Tweet? _getPinnedTweet(Map<String, dynamic> responseJSON) {
|
||||
try {
|
||||
final tweetId = responseJSON['data']['pinned_tweet_id'] as String?;
|
||||
if (tweetId == null || responseJSON['includes'] == null) return null;
|
||||
|
||||
final tweetIncludes = List.from(responseJSON['includes']['tweets'] as List);
|
||||
final pinnedTweetData = tweetIncludes.firstWhere(
|
||||
(tweet) => tweet['id'] == tweetId,
|
||||
orElse: () => null,
|
||||
) as Map<String, dynamic>?;
|
||||
|
||||
if (pinnedTweetData == null) return null;
|
||||
|
||||
final pinnedTweetText =
|
||||
(pinnedTweetData['note_tweet']?['text'] ?? pinnedTweetData['text']) as String;
|
||||
|
||||
return Tweet(id: tweetId, text: pinnedTweetText);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
class TwitterUser {
|
||||
TwitterUser(
|
||||
{required this.id,
|
||||
required this.username,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.location,
|
||||
required this.profileImageUrl,
|
||||
this.pinnedTweet});
|
||||
|
||||
final String id;
|
||||
final String username;
|
||||
final String name;
|
||||
final String description;
|
||||
final String location;
|
||||
final String profileImageUrl;
|
||||
final Tweet? pinnedTweet;
|
||||
|
||||
factory TwitterUser.fromJson(Map<String, dynamic> json, [Tweet? pinnedTweet]) {
|
||||
final profileImageUrl = json['data']['profile_image_url'] as String? ?? '';
|
||||
final scaledProfileImageUrl = profileImageUrl.replaceFirst('normal', '200x200');
|
||||
return TwitterUser(
|
||||
id: json['data']['id'] as String,
|
||||
username: json['data']['username'] as String? ?? '',
|
||||
name: json['data']['name'] as String,
|
||||
description: json['data']['description'] as String? ?? '',
|
||||
location: json['data']['location'] as String? ?? '',
|
||||
profileImageUrl: scaledProfileImageUrl,
|
||||
pinnedTweet: pinnedTweet,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tweet {
|
||||
Tweet({
|
||||
required this.id,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String text;
|
||||
|
||||
factory Tweet.fromJson(Map<String, dynamic> json) {
|
||||
return Tweet(
|
||||
id: json['id'] as String,
|
||||
text: json['text'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/openalias/openalias_record.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/store/settings_store.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';
|
||||
|
||||
class UnstoppableAddressProvider extends AddressLookupProvider {
|
||||
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"
|
||||
];
|
||||
|
||||
@override
|
||||
AddressSource get source => AddressSource.unstoppableDomains;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.xmr, CryptoCurrency.btc];
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsUnstoppableDomains;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) {
|
||||
// Unstoppable Domains handle example: name.crypto
|
||||
final formattedName = OpenaliasRecord.formatDomainName(query);
|
||||
final domainParts = formattedName.split('.');
|
||||
final name = domainParts.last;
|
||||
return domainParts.length > 1 &&
|
||||
domainParts.first.isNotEmpty &&
|
||||
name.isNotEmpty &&
|
||||
unstoppableDomains.any((domain) => name.trim() == domain);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final formattedName = OpenaliasRecord.formatDomainName(query);
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
for (final currency in currencies) {
|
||||
final address = await fetchUnstoppableDomainAddress(
|
||||
formattedName,
|
||||
currency.title,
|
||||
);
|
||||
|
||||
if (address.isNotEmpty) {
|
||||
result[currency] = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
profileName: formattedName,
|
||||
addressSource: AddressSource.unstoppableDomains,
|
||||
handle: formattedName,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving Unstoppable Domain: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/wellknown/wellknown_record.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class WellKnownAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.wellKnown;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.nano];
|
||||
|
||||
@override
|
||||
bool canHandle(String q) => q.contains('.') && q.contains('@');
|
||||
|
||||
// .well-known handle example:
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final rec = await WellKnownRecord.fetch(query, CryptoCurrency.nano);
|
||||
if (rec == null || rec.address.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: {CryptoCurrency.nano: rec.address},
|
||||
addressSource: AddressSource.wellKnown,
|
||||
handle: query,
|
||||
profileName: rec.title ?? '',
|
||||
profileImageUrl: rec.imageUrl ?? '',
|
||||
)
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving Well-Known address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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.alias,
|
||||
required this.imageUrl,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final String address;
|
||||
final String alias;
|
||||
final String? imageUrl;
|
||||
final String? title;
|
||||
|
||||
static const _jsonLocation = {CryptoCurrency.nano: 'nano-currency'};
|
||||
|
||||
static Future<WellKnownRecord?> fetch(
|
||||
String username,
|
||||
CryptoCurrency currency,
|
||||
) async {
|
||||
final location = _jsonLocation[currency];
|
||||
if (location == null) {
|
||||
printV('well-known: unsupported coin $currency');
|
||||
return null;
|
||||
}
|
||||
|
||||
final parts = username.split('@');
|
||||
if (parts.length < 2) {
|
||||
printV('well-known: missing @ in "$username"');
|
||||
return null;
|
||||
}
|
||||
final alias = (parts.length == 3 ? parts[1] : parts[0]).trim();
|
||||
final domain = parts.last.trim();
|
||||
final encoded = Uri.encodeComponent(alias.isEmpty ? '_' : alias);
|
||||
|
||||
final uri = Uri.https(
|
||||
domain,
|
||||
'/.well-known/$location.json',
|
||||
{'names': encoded},
|
||||
);
|
||||
|
||||
try {
|
||||
final res = await ProxyWrapper().get(
|
||||
clearnetUri: uri,
|
||||
headers: {'Accept': 'application/json'},
|
||||
);
|
||||
|
||||
final cType = res.headers['content-type']?.toLowerCase() ?? '';
|
||||
if (res.statusCode != 200 || !cType.contains('application/json')) {
|
||||
printV('well-known: $uri → ${res.statusCode} $cType');
|
||||
return null;
|
||||
}
|
||||
|
||||
final body = json.decode(res.body) as Map<String, dynamic>;
|
||||
final items = body['names'] as List<dynamic>? ?? const [];
|
||||
|
||||
for (final raw in items.whereType<Map>()) {
|
||||
if (raw['name'].toString().toLowerCase() == alias.toLowerCase()) {
|
||||
return WellKnownRecord(
|
||||
address: raw['address']?.toString() ?? '',
|
||||
alias: alias,
|
||||
imageUrl: raw['image']?.toString(),
|
||||
title: raw['name']?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
printV('well-known: alias "$alias" not found in list');
|
||||
} catch (e) {
|
||||
printV('well-known: network / JSON error → $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
|
||||
import 'package:cake_wallet/entities/emoji_string_extension.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class YatAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.yatRecord;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies =>
|
||||
[CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth, CryptoCurrency.ltc];
|
||||
|
||||
@override
|
||||
bool canHandle(String q) => q.hasOnlyEmojis; // Yat handle example: 🐶🐾
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
for (final cur in currencies) {
|
||||
final records = await YatService.fetchYatAddress(query, cur.title);
|
||||
if (records.isEmpty) continue;
|
||||
|
||||
final chosen = cur == CryptoCurrency.xmr
|
||||
? records.firstWhere((r) => r.isMoneroSub, orElse: () => records.first)
|
||||
: records.first;
|
||||
|
||||
result[cur] = chosen.address;
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.yatRecord,
|
||||
handle: query,
|
||||
)
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving Yat address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
|
||||
|
||||
class YatRecord {
|
||||
YatRecord({
|
||||
required this.address,
|
||||
required this.tag,
|
||||
});
|
||||
|
||||
final String address;
|
||||
final String tag;
|
||||
|
||||
factory YatRecord.fromJson(Map<String, dynamic> json, String tag) =>
|
||||
YatRecord(address: (json['address'] ?? '').toString(), tag: tag);
|
||||
|
||||
bool get isMoneroSub => tag == YatService.MONERO_SUB_ADDRESS;
|
||||
|
||||
@override
|
||||
String toString() => 'YatRecord(tag: $tag, address: $address)';
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/core/address_resolver/yat/yat_record.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
class YatService {
|
||||
static bool isDevMode = false;
|
||||
|
||||
static String get apiUrl =>
|
||||
YatService.isDevMode ? YatService.apiDevUrl : YatService.apiReleaseUrl;
|
||||
static const apiReleaseUrl = "https://a.y.at";
|
||||
static const apiDevUrl = 'https://a.yat.fyi';
|
||||
|
||||
static String lookupEmojiUrl(String emojiId) =>
|
||||
"$apiUrl/emoji_id/$emojiId/payment";
|
||||
|
||||
static const String MONERO_SUB_ADDRESS = '0x1002';
|
||||
static const String MONERO_STD_ADDRESS = '0x1001';
|
||||
static const tags = {
|
||||
'XMR': "$MONERO_STD_ADDRESS,$MONERO_SUB_ADDRESS",
|
||||
'BTC': '0x1003',
|
||||
'LTC': '0x1019'
|
||||
};
|
||||
|
||||
static Future<List<YatRecord>> fetchYatAddress(
|
||||
String emojiId,
|
||||
String ticker,
|
||||
) async {
|
||||
final tagQuery = tags[ticker.toUpperCase()];
|
||||
if (tagQuery == null) return const [];
|
||||
|
||||
final uri = Uri.parse(
|
||||
lookupEmojiUrl(emojiId.replaceAll(' ', '')),
|
||||
).replace(queryParameters: {'tags': tagQuery});
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final res = body['result'];
|
||||
if (res == null) return const [];
|
||||
|
||||
final records = <YatRecord>[];
|
||||
|
||||
if (res is Map) {
|
||||
res.forEach((tag, data) {
|
||||
if (data is Map<String, dynamic>) {
|
||||
records.add(YatRecord.fromJson(data, tag.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
else if (res is List) {
|
||||
for (final item in res) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
final tag = item['tag']?.toString() ?? '';
|
||||
records.add(YatRecord.fromJson(item, tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import 'package:cake_wallet/core/secure_storage.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'dart:async';
|
||||
|
||||
part 'yat_store.g.dart';
|
||||
|
||||
class YatLink {
|
||||
static const partnerId = ''; // 'CW';
|
||||
static const baseDevUrl = ''; // 'https://yat.fyi';
|
||||
static const baseReleaseUrl = ''; // 'https://y.at';
|
||||
static const signInSuffix = ''; // '/partner/$partnerId/link-email';
|
||||
static const createSuffix = ''; // '/create';
|
||||
static const managePath = ''; // '/partner/$partnerId/manage';
|
||||
static const queryParameter = ''; // '?address_json=';
|
||||
static const apiDevUrl = ''; // 'https://a.yat.fyi';
|
||||
static const apiReleaseUrl = ''; // 'https://a.y.at';
|
||||
static const requestDevUrl = ''; // 'https://a.yat.fyi/emoji_id/';
|
||||
static const requestReleaseUrl = ''; //'https://a.y.at/emoji_id/';
|
||||
static const startFlowUrl = ''; // 'https://www.y03btrk.com/4RQSJ/6JHXF/';
|
||||
static const isDevMode = true;
|
||||
static const tags = <String, List<String>>{"XMR" : ['0x1001', '0x1002'],
|
||||
"BTC" : ['0x1003'], "LTC" : ['0x1019']};
|
||||
|
||||
static String get apiUrl => YatLink.isDevMode
|
||||
? YatLink.apiDevUrl
|
||||
: YatLink.apiReleaseUrl;
|
||||
|
||||
static String get emojiIdUrl => apiUrl + '/emoji_id/';
|
||||
|
||||
static String get baseUrl => YatLink.isDevMode
|
||||
? YatLink.baseDevUrl
|
||||
: YatLink.baseReleaseUrl;
|
||||
}
|
||||
|
||||
Future<List<String>> fetchYatAddress(String emojiId, String ticker) async {
|
||||
throw Exception();
|
||||
//final url = YatLink.emojiIdUrl + emojiId + '/payment';
|
||||
//final response = await get(url);
|
||||
|
||||
//if (response.statusCode != 200) {
|
||||
// throw YatException(text: response.body.toString());
|
||||
//}
|
||||
|
||||
//final addresses = <String>[];
|
||||
//final currency = ticker.toUpperCase();
|
||||
//final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
//final result = responseJSON['result'] as Map<dynamic, dynamic>;
|
||||
//result.forEach((dynamic key, dynamic value) {
|
||||
// final tag = key as String ?? '';
|
||||
// final record = value as Map<String, dynamic>;
|
||||
|
||||
// if (YatLink.tags[currency]?.contains(tag) ?? false) {
|
||||
// final address = record['address'] as String;
|
||||
// if (address?.isNotEmpty ?? false) {
|
||||
// addresses.add(address);
|
||||
// }
|
||||
// }
|
||||
//});
|
||||
|
||||
//return addresses;
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<String> fetchYatAccessToken(String refreshToken) async {
|
||||
throw Exception();
|
||||
//try {
|
||||
// final url = YatLink.apiUrl + '/auth/token/refresh';
|
||||
// final bodyJson = json.encode({'refresh_token': refreshToken});
|
||||
// final response = await post(
|
||||
// url,
|
||||
// headers: <String, String>{
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Accept': '*/*'
|
||||
// },
|
||||
// body: bodyJson);
|
||||
|
||||
// if (response.statusCode != 200) {
|
||||
// throw YatException(text: response.body.toString());
|
||||
// }
|
||||
|
||||
// final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
// return responseJSON['access_token'] as String;
|
||||
//}catch(_) {
|
||||
// return '';
|
||||
//}
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<String> fetchYatApiKey(String accessKey) async {
|
||||
throw Exception();
|
||||
//try {
|
||||
// final url = YatLink.apiUrl + '/api_keys';
|
||||
// final bodyJson = json.encode({'name': 'CW'});
|
||||
// final response = await post(
|
||||
// url,
|
||||
// headers: <String, String>{
|
||||
// 'Authorization': 'Bearer $accessKey',
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Accept': '*/*'
|
||||
// },
|
||||
// body: bodyJson);
|
||||
|
||||
// if (response.statusCode != 200) {
|
||||
// throw YatException(text: response.body.toString());
|
||||
// }
|
||||
|
||||
// final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
// return responseJSON['api_key'] as String;
|
||||
//}catch(_) {
|
||||
// return '';
|
||||
//}
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<void> updateEmojiIdAddress(String emojiId, String address, String apiKey, WalletType type) async {
|
||||
throw Exception();
|
||||
//final url = YatLink.emojiIdUrl + emojiId;
|
||||
//final cur = walletTypeToCryptoCurrency(type);
|
||||
//final curFormatted = cur.toString().toUpperCase();
|
||||
//var tag = '';
|
||||
|
||||
//if (type == WalletType.monero && !address.startsWith('4')) {
|
||||
// tag = YatLink.tags[curFormatted].last;
|
||||
//} else {
|
||||
// tag = YatLink.tags[curFormatted].first;
|
||||
//}
|
||||
|
||||
//final bodyJson = json.encode({
|
||||
// 'insert': [{
|
||||
// 'data': address,
|
||||
// 'tag': tag
|
||||
// }]
|
||||
//});
|
||||
//final response = await patch(
|
||||
// url,
|
||||
// headers: <String, String>{
|
||||
// 'x-api-key': apiKey,
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Accept': '*/*'
|
||||
// },
|
||||
// body: bodyJson);
|
||||
|
||||
//if (response.statusCode != 200) {
|
||||
// throw YatException(text: response.body.toString());
|
||||
//}
|
||||
}
|
||||
|
||||
Future<String> visualisationForEmojiId(String emojiId) async {
|
||||
throw Exception();
|
||||
//final url = YatLink.emojiIdUrl + emojiId + '/json/VisualizerFileLocations';
|
||||
//final response = await get(url);
|
||||
//final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
//final data = responseJSON['data'] as Map<String, dynamic>;
|
||||
//final result = data['gif'] as String ?? '';
|
||||
//return result;
|
||||
return '';
|
||||
}
|
||||
|
||||
class YatStore = YatStoreBase with _$YatStore;
|
||||
|
||||
abstract class YatStoreBase with Store {
|
||||
YatStoreBase({
|
||||
required this.appStore,
|
||||
required this.secureStorage})
|
||||
: _wallet = appStore.wallet,
|
||||
emoji = appStore.wallet?.walletInfo?.yatEmojiId ?? '',
|
||||
refreshToken = '',
|
||||
accessToken = '',
|
||||
apiKey = '',
|
||||
emojiIncommingSC = StreamController<String>.broadcast() {
|
||||
//reaction((_) => appStore.wallet, _onWalletChange);
|
||||
//reaction((_) => emoji, (String _) => _onEmojiChange());
|
||||
//reaction((_) => refreshToken, (String _) => _onRefreshTokenChange());
|
||||
}
|
||||
|
||||
static const yatRefreshTokenKeyBase = 'yat_refresh_token';
|
||||
static const yatAccessTokenKeyBase = 'yat_access_token';
|
||||
static const yatApiKeyBase = 'yat_api_key';
|
||||
|
||||
static String yatRefreshTokenKey(String name) => '${yatRefreshTokenKeyBase}_$name';
|
||||
static String yatAccessTokenKey(String name) => '${yatAccessTokenKeyBase}_$name';
|
||||
static String yatApiKey(String name) => '${yatApiKeyBase}_$name';
|
||||
|
||||
AppStore appStore;
|
||||
|
||||
SecureStorage secureStorage;
|
||||
|
||||
@observable
|
||||
String emoji;
|
||||
|
||||
@observable
|
||||
String refreshToken;
|
||||
|
||||
@observable
|
||||
String accessToken;
|
||||
|
||||
@observable
|
||||
String apiKey;
|
||||
|
||||
StreamController<String> emojiIncommingSC;
|
||||
|
||||
Stream<String> get emojiIncommingStream => emojiIncommingSC.stream;
|
||||
|
||||
@observable
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
|
||||
_wallet;
|
||||
|
||||
Future<void> init() async {
|
||||
if (_wallet == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshToken = await secureStorage.read(key: yatRefreshTokenKey(_wallet!.walletInfo.name)) ?? '';
|
||||
accessToken = await secureStorage.read(key: yatAccessTokenKey(_wallet!.walletInfo.name)) ?? '';
|
||||
apiKey = await secureStorage.read(key: yatApiKey(_wallet!.walletInfo.name)) ?? '';
|
||||
}
|
||||
|
||||
@action
|
||||
void _onWalletChange(
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
|
||||
TransactionInfo>
|
||||
wallet) {
|
||||
this._wallet = wallet;
|
||||
emoji = wallet?.walletInfo?.yatEmojiId ?? '';
|
||||
init();
|
||||
}
|
||||
|
||||
@action
|
||||
void _onEmojiChange() {
|
||||
try {
|
||||
final walletInfo = _wallet?.walletInfo;
|
||||
|
||||
if (walletInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
walletInfo!.yatEid = emoji;
|
||||
|
||||
walletInfo!.save();
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> _onRefreshTokenChange() async {
|
||||
throw Exception();
|
||||
|
||||
//try {
|
||||
// await secureStorage.write(key: yatRefreshTokenKey(_wallet.walletInfo.name), value: refreshToken);
|
||||
// accessToken = await fetchYatAccessToken(refreshToken);
|
||||
// await secureStorage.write(key: yatAccessTokenKey(_wallet.walletInfo.name), value: accessToken);
|
||||
// apiKey = await fetchYatApiKey(accessToken);
|
||||
// await secureStorage.write(key: yatApiKey(_wallet.walletInfo.name), value: accessToken);
|
||||
//} catch (e) {
|
||||
// printV(e.toString());
|
||||
//}
|
||||
}
|
||||
|
||||
String defineQueryParameters() {
|
||||
throw Exception();
|
||||
//final result = <String, String>{};
|
||||
//final tags = YatLink.tags[_wallet.currency.toString().toUpperCase()];
|
||||
//String tag = tags.first;
|
||||
|
||||
//if (_wallet.type == WalletType.monero
|
||||
// && _wallet.walletAddresses.address.startsWith('4')) {
|
||||
// tag = tags.last;
|
||||
//}
|
||||
//result[tag] = '${_wallet.walletAddresses.address}|${_wallet.name}';
|
||||
//final addressJson = json.encode([result]);
|
||||
//final addressJsonBytes = utf8.encode(addressJson);
|
||||
|
||||
//return base64.encode(addressJsonBytes);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/store/settings_store.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';
|
||||
|
||||
class ZanoAliasAddressProvider extends AddressLookupProvider {
|
||||
|
||||
@override
|
||||
AddressSource get source => AddressSource.zanoAlias;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zano];
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZanoAlias;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) => query.startsWith('@'); // zano handle example: @username
|
||||
|
||||
static const _zanoUrl = 'http://37.27.100.59:10500/json_rpc';
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
if (!currencies.contains(CryptoCurrency.zano)) return [];
|
||||
|
||||
final formattedName = query.replaceFirst("@", "");
|
||||
if (formattedName.isEmpty) return [];
|
||||
|
||||
final zanoAddress = await fetchZanoAliasAddress(formattedName);
|
||||
if (zanoAddress == null || zanoAddress.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: {CryptoCurrency.zano: zanoAddress},
|
||||
addressSource: AddressSource.zanoAlias,
|
||||
handle: query,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving Zano Alias: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> fetchZanoAliasAddress(String alias) async {
|
||||
try {
|
||||
final uri = Uri.parse(_zanoUrl);
|
||||
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('[address resolver] Zano Alias error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/zcash/zcash_names_record.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class ZcashNameAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.zcashName;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zec];
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) {
|
||||
final lowerText = query.toLowerCase();
|
||||
return lowerText.endsWith(".zec") || lowerText.endsWith(".zcash");
|
||||
} // Zcash handle example: name.zec
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final address = await ZcashNamesRecord.fetchZcashNamesAddress(query);
|
||||
if (address == null || address.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: {CryptoCurrency.zec: address},
|
||||
profileName: query,
|
||||
addressSource: AddressSource.zcashName,
|
||||
handle: query,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving Zcash Name: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/store/settings_store.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';
|
||||
|
||||
class ZcashMeAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.zcashAddress;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zec];
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) {
|
||||
final value = query.trim().toLowerCase();
|
||||
return value.startsWith('zcash.me/') && value.split('/').last.isNotEmpty;
|
||||
} // zcash.me profile example: zcash.me/username
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
try {
|
||||
final handle = query.trim().split('/').last;
|
||||
if (handle.isEmpty) return [];
|
||||
|
||||
final address = await _fetchZcashAddress(handle);
|
||||
if (address == null || address.isEmpty) return [];
|
||||
|
||||
return [
|
||||
ParsedAddress(
|
||||
parsedAddressByCurrencyMap: {CryptoCurrency.zec: address},
|
||||
profileName: handle,
|
||||
addressSource: AddressSource.zcashAddress,
|
||||
handle: query,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error resolving zcash.me address: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return match?.group(0);
|
||||
}
|
||||
} catch (e) {
|
||||
printV('[address resolver] Error fetching zcash.me profile: $e');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -12,29 +12,54 @@ const AFTER_REGEX = '(\$|\\s)';
|
||||
class AddressValidator extends TextValidator {
|
||||
AddressValidator({required CryptoCurrency type, bool isTestnet = false})
|
||||
: super(
|
||||
errorMessage: S.current.error_text_address,
|
||||
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
|
||||
? (String txt) {
|
||||
final RegExp lightningInvoiceRegex = RegExp(
|
||||
r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$',
|
||||
caseSensitive: false);
|
||||
if (lightningInvoiceRegex.hasMatch(txt)) return true;
|
||||
errorMessage: S.current.error_text_address,
|
||||
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
|
||||
? (String txt) {
|
||||
final RegExp lightningInvoiceRegex = RegExp(
|
||||
r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$',
|
||||
caseSensitive: false);
|
||||
if (lightningInvoiceRegex.hasMatch(txt)) return true;
|
||||
|
||||
return BitcoinAddressUtils.validateAddress(
|
||||
address: txt,
|
||||
network: type == CryptoCurrency.btc
|
||||
? isTestnet
|
||||
? BitcoinNetwork.testnet
|
||||
: BitcoinNetwork.mainnet
|
||||
: LitecoinNetwork.mainnet,
|
||||
);
|
||||
}
|
||||
: type == CryptoCurrency.zano
|
||||
? zano?.validateAddress
|
||||
: null,
|
||||
pattern: getPattern(type, isTestnet: isTestnet),
|
||||
length: getLength(type),
|
||||
);
|
||||
return BitcoinAddressUtils.validateAddress(
|
||||
address: txt,
|
||||
network: type == CryptoCurrency.btc
|
||||
? isTestnet
|
||||
? BitcoinNetwork.testnet
|
||||
: BitcoinNetwork.mainnet
|
||||
: LitecoinNetwork.mainnet,
|
||||
);
|
||||
}
|
||||
: type == CryptoCurrency.zano
|
||||
? zano?.validateAddress
|
||||
: null,
|
||||
pattern: getPattern(type, isTestnet: isTestnet),
|
||||
length: getLength(type),
|
||||
);
|
||||
|
||||
static const List<CryptoCurrency> reliableValidateCurrencies = [
|
||||
CryptoCurrency.xmr,
|
||||
CryptoCurrency.btc,
|
||||
CryptoCurrency.ltc,
|
||||
CryptoCurrency.bch,
|
||||
CryptoCurrency.trx,
|
||||
CryptoCurrency.nano,
|
||||
CryptoCurrency.sol,
|
||||
CryptoCurrency.zano,
|
||||
CryptoCurrency.ada,
|
||||
CryptoCurrency.xrp,
|
||||
CryptoCurrency.xhv,
|
||||
CryptoCurrency.zaddr,
|
||||
CryptoCurrency.zec,
|
||||
CryptoCurrency.dcr,
|
||||
CryptoCurrency.rvn,
|
||||
CryptoCurrency.near,
|
||||
CryptoCurrency.rune,
|
||||
CryptoCurrency.scrt,
|
||||
CryptoCurrency.stx,
|
||||
CryptoCurrency.kmd,
|
||||
CryptoCurrency.doge,
|
||||
CryptoCurrency.btcln,
|
||||
];
|
||||
|
||||
static String getPattern(CryptoCurrency type, {bool isTestnet = false}) {
|
||||
var pattern = "";
|
||||
@@ -146,13 +171,13 @@ class AddressValidator extends TextValidator {
|
||||
pattern = '[0-9a-zA-Z.]+';
|
||||
case CryptoCurrency.zec:
|
||||
pattern =
|
||||
'(?:'
|
||||
't1[0-9A-Za-z]{33}'
|
||||
'|t3[0-9A-Za-z]{33}'
|
||||
'|zs[a-z0-9]{76}'
|
||||
'|zxviews[a-z0-9]{278}'
|
||||
'|u1[a-z0-9]{1,300}'
|
||||
')';
|
||||
'(?:'
|
||||
't1[0-9A-Za-z]{33}'
|
||||
'|t3[0-9A-Za-z]{33}'
|
||||
'|zs[a-z0-9]{76}'
|
||||
'|zxviews[a-z0-9]{278}'
|
||||
'|u1[a-z0-9]{1,300}'
|
||||
')';
|
||||
case CryptoCurrency.dcr:
|
||||
pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+';
|
||||
case CryptoCurrency.rvn:
|
||||
@@ -344,7 +369,7 @@ class AddressValidator extends TextValidator {
|
||||
'|([0-9a-zA-Z]{106})';
|
||||
case CryptoCurrency.btc:
|
||||
pattern =
|
||||
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
|
||||
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
|
||||
case CryptoCurrency.ltc:
|
||||
pattern = '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
@@ -394,4 +419,4 @@ class AddressValidator extends TextValidator {
|
||||
|
||||
return pattern != null ? "($pattern)" : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/entities/yat_record.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
class YatService {
|
||||
static bool isDevMode = false;
|
||||
|
||||
static String get apiUrl =>
|
||||
YatService.isDevMode ? YatService.apiDevUrl : YatService.apiReleaseUrl;
|
||||
static const apiReleaseUrl = "https://a.y.at";
|
||||
static const apiDevUrl = 'https://a.yat.fyi';
|
||||
|
||||
static String lookupEmojiUrl(String emojiId) =>
|
||||
"$apiUrl/emoji_id/$emojiId/payment";
|
||||
|
||||
static const String MONERO_SUB_ADDRESS = '0x1002';
|
||||
static const String MONERO_STD_ADDRESS = '0x1001';
|
||||
static const tags = {
|
||||
'XMR': "$MONERO_STD_ADDRESS,$MONERO_SUB_ADDRESS",
|
||||
'BTC': '0x1003',
|
||||
'LTC': '0x1019'
|
||||
};
|
||||
|
||||
Future<List<YatRecord>> fetchYatAddress(String emojiId, String ticker) async {
|
||||
final formattedTicker = ticker.toUpperCase();
|
||||
final formattedEmojiId = emojiId.replaceAll(' ', '');
|
||||
final tag = tags[formattedTicker];
|
||||
final uri = Uri.parse(lookupEmojiUrl(formattedEmojiId)).replace(
|
||||
queryParameters: <String, dynamic>{
|
||||
"tags": tag
|
||||
});
|
||||
final yatRecords = <YatRecord>[];
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
final resBody = json.decode(response.body) as Map<String, dynamic>;
|
||||
final results = resBody["result"] as Map<dynamic, dynamic>;
|
||||
// Favour a subaddress over a standard address.
|
||||
final yatRecord = (
|
||||
results[MONERO_SUB_ADDRESS] ??
|
||||
results[MONERO_STD_ADDRESS] ??
|
||||
results[tag]) as Map<String, dynamic>;
|
||||
|
||||
if (yatRecord.isNotEmpty) {
|
||||
yatRecords.add(YatRecord.fromJson(yatRecord));
|
||||
}
|
||||
|
||||
return yatRecords;
|
||||
} catch (_) {
|
||||
return yatRecords;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user