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:
Serhii
2026-07-06 16:48:30 +03:00
committed by GitHub
co-authored by Omar Hatem malik1004x
parent 00b7d7ce0d
commit 7928c912e8
93 changed files with 3044 additions and 1796 deletions
@@ -11,8 +11,8 @@ import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/routes.dart';
@@ -584,8 +584,11 @@ class CakePayBuyCardPage extends BasePage {
final displayingOutputs = _sendViewModel.outputs
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
addresses: [o.address],
name: 'Cake Pay',
parsedAddressByCurrencyMap: {
cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency:
o.address,
},
handle: 'Cake Pay',
profileName: order?.cards.first.cardName ?? 'Cake Pay',
profileImageUrl: order?.cards.first.cardImagePath ?? '',
),
@@ -653,8 +656,11 @@ class CakePayBuyCardPage extends BasePage {
final displayingOutputs = outputsCopy
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
addresses: [o.address],
name: 'Cake Pay',
parsedAddressByCurrencyMap: {
cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency:
o.address,
},
handle: 'Cake Pay',
profileName: order?.cards.first.cardName ?? 'Cake Pay',
profileImageUrl: order?.cards.first.cardImagePath ?? '',
),
@@ -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 [];
}
}
}
@@ -2,13 +2,9 @@ import 'dart:convert';
import 'dart:isolate';
import 'package:basic_utils/basic_utils.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:dnssec_proof/dnssec_proof.dart';
import 'package:flutter/material.dart';
class Bip353Record {
Bip353Record({
@@ -104,69 +100,4 @@ class Bip353Record {
}
}
static Future<String?> pickBip353AddressChoice(
BuildContext context,
String bip353Name,
Map<String, String> addressMap,
) async {
if (addressMap.length == 1) {
return addressMap.values.first;
}
final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap);
return chosenAddress;
}
static Future<String?> _showAddressChoiceDialog(
BuildContext context,
String bip353Name,
Map<String, String> addressMap,
) async {
final entriesList = addressMap.entries.toList();
final List<Map<String, String>> displayItems = entriesList.map((entry) {
final originalKey = entry.key;
final originalValue = entry.value;
final extendedKeyName = keyDisplayMap[originalKey] ?? originalKey;
final truncatedValue = _truncate(originalValue, front: 6, back: 6);
return {
'displayKey': extendedKeyName,
'displayValue': truncatedValue,
'originalKey': originalKey,
'originalValue': originalValue,
};
}).toList();
String? selectedOriginalValue;
if (context.mounted) {
await showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithPickerOption(
alertTitle: S.of(context).multiple_addresses_detected + '\n$bip353Name',
alertTitleTextSize: 14,
alertSubtitle: S.of(context).please_choose_one + ':',
options: displayItems,
onOptionSelected: (Map<String, String> chosenItem) {
selectedOriginalValue = chosenItem['originalValue'];
},
alertBarrierDismissible: true,
);
},
);
}
return selectedOriginalValue;
}
static String _truncate(String value, {int front = 6, int back = 6}) {
if (value.length <= front + back) return value;
final start = value.substring(0, front);
final end = value.substring(value.length - back);
return '$start...$end';
}
}
}
@@ -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,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 [];
}
}
}
@@ -1,7 +1,7 @@
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';
import 'package:cake_wallet/mastodon/mastodon_user.dart';
class MastodonAPI {
static const httpsScheme = 'https';
@@ -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,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,
);
}
}
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/twitter/twitter_user.dart';
import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class TwitterApi {
@@ -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,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;
}
}
+56 -31
View File
@@ -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;
}
}
}
-55
View File
@@ -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;
}
}
}
+17 -10
View File
@@ -6,6 +6,9 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
@@ -29,7 +32,6 @@ import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/decred/decred.dart';
import 'package:cake_wallet/entities/biometric_auth.dart';
import 'package:cake_wallet/entities/bridge_transfer.dart';
@@ -37,7 +39,6 @@ import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/entities/template.dart';
@@ -198,7 +199,6 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
import 'package:cake_wallet/store/templates/send_template_store.dart';
import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/themes/core/theme_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/utils/device_info.dart';
@@ -899,6 +899,7 @@ Future<void> setup({
getIt.get<AppStore>(),
getIt.get<SendTemplateViewModel>(),
getIt.get<FiatConversionStore>(),
getIt.get<AddressResolverService>(),
getIt.get<BalanceViewModel>(),
getIt.get<ContactListViewModel>(),
_transactionDescriptionBox,
@@ -1100,7 +1101,10 @@ Future<void> setup({
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
(ContactRecord? contact, _) => ContactViewModel(_contactSource,
getIt.get<AppStore>(),
getIt.get<AddressResolverService>(),
contact: contact));
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
(CryptoCurrency? cur, _) =>
@@ -1249,7 +1253,10 @@ Future<void> setup({
getIt.registerFactoryParam<ExchangePage, PaymentRequest?, void>(
(PaymentRequest? paymentRequest, __) {
return ExchangePage(getIt.get<ExchangeViewModel>(), getIt.get<AuthService>(), paymentRequest);
return ExchangePage(getIt.get<ExchangeViewModel>(),
getIt.get<AuthService>(),
getIt.get<AddressResolverService>(),
paymentRequest);
});
getIt.registerFactoryParam<NewSwapPage, PaymentRequest?, CryptoCurrency?>(
@@ -1257,6 +1264,7 @@ Future<void> setup({
return NewSwapPage(
getIt.get<ExchangeViewModel>(),
getIt.get<AuthService>(),
getIt.get<AddressResolverService>(),
paymentRequest,
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
initialCurrency: initialCurrency,
@@ -1494,7 +1502,7 @@ Future<void> setup({
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>(), getIt.get<AddressResolverService>()));
getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableItem>;
@@ -1592,10 +1600,9 @@ Future<void> setup({
getIt.registerFactory(() => YatService());
getIt.registerFactory(() => AddressResolver(
yatService: getIt.get<YatService>(),
wallet: getIt.get<AppStore>().wallet!,
settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory<AddressResolverService>(
() => AddressResolverService(settingsStore: getIt.get<SettingsStore>()),
);
getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>(
(QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData));
-54
View File
@@ -1,54 +0,0 @@
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:ens_dart/ens_dart.dart';
import 'package:web3dart/web3dart.dart';
class EnsRecord {
static Future<String> fetchEnsAddress(String name, {WalletBase? wallet}) async {
Web3Client? _client;
if (wallet != null && (isEVMCompatibleChain(wallet.type))) {
_client = evm!.getWeb3Client(wallet);
}
if (_client == null) {
late final client = ProxyWrapper().getHttpIOClient();
_client = Web3Client("https://ethereum-rpc.publicnode.com", client);
}
try {
final ens = Ens(client: _client);
if (wallet != null) {
switch (wallet.type) {
case WalletType.monero:
return await ens.withName(name).getCoinAddress(CoinType.XMR);
case WalletType.bitcoin:
return await ens.withName(name).getCoinAddress(CoinType.BTC);
case WalletType.litecoin:
return await ens.withName(name).getCoinAddress(CoinType.LTC);
case WalletType.haven:
return await ens.withName(name).getCoinAddress(CoinType.XHV);
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
default:
return (await ens.withName(name).getAddress()).hex;
}
}
final addr = await ens.withName(name).getAddress();
return addr.hex;
} catch (e) {
printV(e);
return "";
}
}
}
-65
View File
@@ -1,65 +0,0 @@
import 'dart:convert';
import 'package:cw_core/utils/proxy_wrapper.dart';
class FioAddressProvider {
static const apiAuthority = 'fio.blockpane.com';
static const availCheck = '/v1/chain/avail_check';
static const getAddress = '/v1/chain/get_pub_address';
static Future<bool> checkAvail(String fioAddress) async {
bool isFioRegistered = false;
final headers = {'Content-Type': 'application/json'};
final body = <String, String>{"fio_name": fioAddress};
final uri = Uri.https(apiAuthority, availCheck);
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode(body),
);
if (response.statusCode != 200) {
return isFioRegistered;
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
isFioRegistered = responseJSON['is_registered'] as int == 1;
return isFioRegistered;
}
static Future<String> getPubAddress(String fioAddress, String token) async {
final headers = {'Content-Type': 'application/json'};
final body = <String, String>{
"fio_address": fioAddress,
"chain_code": token.toUpperCase(),
"token_code": token.toUpperCase(),
};
final uri = Uri.https(apiAuthority, getAddress);
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode(body),
);
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error'] as String;
final message = responseJSON['message'] as String;
throw Exception('${error}\n$message');
}
if (response.statusCode != 200) {
throw Exception('Unexpected response http status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final String pubAddress = responseJSON['public_address'] as String;
return pubAddress;
}
}
-89
View File
@@ -1,89 +0,0 @@
import 'package:basic_utils/basic_utils.dart';
import 'package:cw_core/utils/print_verbose.dart';
class OpenaliasRecord {
OpenaliasRecord({
required this.address,
required this.name,
required this.description,
});
final String name;
final String address;
final String description;
static String formatDomainName(String name) {
String formattedName = name;
if (name.contains("@")) {
formattedName = name.replaceAll("@", ".");
}
return formattedName;
}
static Future<List<RRecord>?> lookupOpenAliasRecord(String name) async {
try {
final txtRecord = await DnsUtils.lookupRecord(name, RRecordType.TXT, dnssec: true);
return txtRecord;
} catch (e) {
printV("${e.toString()}");
return null;
}
}
static OpenaliasRecord fetchAddressAndName({
required String formattedName,
required String ticker,
required List<RRecord> txtRecord,
}) {
String address = '';
String name = formattedName;
String note = '';
for (RRecord element in txtRecord) {
String record = element.data;
if (record.contains("oa1:$ticker") && record.contains("recipient_address")) {
record = record.replaceAll('\"', "");
final dataList = record.split(";");
address = dataList
.where((item) => (item.contains("recipient_address")))
.toString()
.replaceAll("oa1:$ticker recipient_address=", "")
.replaceAll("(", "")
.replaceAll(")", "")
.trim();
final recipientName = dataList
.where((item) => (item.contains("recipient_name")))
.toString()
.replaceAll("(", "")
.replaceAll(")", "")
.trim();
if (recipientName.isNotEmpty) {
name = recipientName.replaceAll("recipient_name=", "");
}
final description = dataList
.where((item) => (item.contains("tx_description")))
.toString()
.replaceAll("(", "")
.replaceAll(")", "")
.trim();
if (description.isNotEmpty) {
note = description.replaceAll("tx_description=", "");
}
break;
}
}
return OpenaliasRecord(address: address, name: name, description: note);
}
}
-549
View File
@@ -1,549 +0,0 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/emoji_string_extension.dart';
import 'package:cake_wallet/entities/ens_record.dart';
import 'package:cake_wallet/entities/lnurlpay_record.dart';
import 'package:cake_wallet/entities/fio_address_provider.dart';
import 'package:cake_wallet/entities/openalias_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/entities/unstoppable_domain_address.dart';
import 'package:cake_wallet/entities/wellknown_record.dart';
import 'package:cake_wallet/entities/zano_alias.dart';
import 'package:cake_wallet/entities/zcash_names_record.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/mastodon/mastodon_api.dart';
import 'package:cake_wallet/nostr/nostr_api.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/twitter/twitter_api.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'bip_353_record.dart';
class AddressResolver {
AddressResolver({required this.yatService, required this.wallet, required this.settingsStore})
: walletType = wallet.type;
final YatService yatService;
final WalletType walletType;
final WalletBase wallet;
final SettingsStore settingsStore;
static const unstoppableDomains = [
"888",
"academy",
"agency",
"altimist",
"anime",
"austin",
"bald",
"bay",
"benji",
"bet",
"binanceus",
"bitcoin",
"bitget",
"bitscrunch",
"blockchain",
"boomer",
"boston",
"ca",
"caw",
"cc",
"chat",
"chomp",
"clay",
"club",
"co",
"com",
"company",
"crypto",
"dao",
"design",
"dfz",
"digital",
"doga",
"donut",
"dream",
"email",
"emir",
"eth",
"ethermail",
"family",
"farms",
"finance",
"fun",
"fyi",
"games",
"global",
"go",
"group",
"guru",
"hi",
"hockey",
"host",
"info",
"io",
"klever",
"kresus",
"kryptic",
"lfg",
"life",
"live",
"llc",
"ltc",
"ltd",
"manga",
"me",
"media",
"metropolis",
"miami",
"miku",
"money",
"moon",
"mumu",
"net",
"network",
"news",
"nft",
"npc",
"onchain",
"online",
"org",
"podcast",
"pog",
"polygon",
"base",
"arbitrum"
"press",
"privacy",
"pro",
"propykeys",
"pudgy",
"pw",
"quantum",
"rad",
"raiin",
"retardio",
"rip",
"rocks",
"secret",
"services",
"site",
"smobler",
"social",
"solutions",
"space",
"stepn",
"store",
"studio",
"systems",
"tball",
"tea",
"team",
"tech",
"technology",
"today",
"tribe",
"u",
"ubu",
"uno",
"unstoppable",
"vip",
"wallet",
"website",
"wif",
"wifi",
"witg",
"work",
"world",
"wrkx",
"wtf",
"x",
"xmr",
"xyz",
"zil",
"zone",
"pizza"
];
static String? extractAddressByType(
{required String raw,
required CryptoCurrency type,
bool requireSurroundingWhitespaces = true}) {
var addressPattern = AddressValidator.getAddressFromStringPattern(type);
if (addressPattern == null) {
return null;
}
if (requireSurroundingWhitespaces)
addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX";
final match = RegExp(addressPattern, multiLine: true).firstMatch(raw);
return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'),
(Match match) {
String group = match.group(0)!;
if (group.startsWith('bitcoincash:') ||
group.startsWith('nano_') ||
group.startsWith('ban_')) {
return group;
}
return '';
});
}
static String extractUnstoppableDomain(String raw) {
// sort by length to avoid matching shorter tld instead of longer
final domains = List<String>.from(unstoppableDomains)..sort((a, b) => b.length.compareTo(a.length));
for (final tld in domains) {
final pattern = RegExp(r'([A-Za-z0-9-]+)\.' + RegExp.escape(tld), caseSensitive: false);
final match = pattern.firstMatch(raw);
if (match != null) return match.group(0)!;
}
return '';
}
bool isEmailFormat(String address) {
final RegExp emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
caseSensitive: false,
);
return emailRegex.hasMatch(address);
}
String _stripHtmlTags(String value) {
return value
.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
.replaceAll(RegExp(r'</p\s*>', caseSensitive: false), '\n')
.replaceAll(RegExp(r'<[^>]*>'), '')
.trim();
}
Future<ParsedAddress> resolve(BuildContext context, String text, CryptoCurrency currency) async {
final ticker = currency.title;
try {
if (text.startsWith("zcash.me")) {
final parts = text.split("/");
final handle = parts.last;
if (parts.length == 2 && handle.isNotEmpty) {
final extractZcashAddress = await _fetchZcashAddress(handle);
if (extractZcashAddress != null) {
return ParsedAddress.zcashAddress(
address: extractZcashAddress,
name: handle,
);
}
}
}
// twitter handle example: @username
if (text.startsWith('@') && !text.substring(1).contains('@')) {
if (currency == CryptoCurrency.zano && settingsStore.lookupsZanoAlias) {
final formattedName = text.substring(1);
final zanoAddress = await ZanoAlias.fetchZanoAliasAddress(formattedName);
if (zanoAddress != null && zanoAddress.isNotEmpty) {
return ParsedAddress.zanoAddress(
address: zanoAddress,
name: text,
);
}
}
if (settingsStore.lookupsTwitter) {
final formattedName = text.substring(1);
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
final addressFromBio = extractAddressByType(
raw: twitterUser.description,
type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency));
if (addressFromBio != null && addressFromBio.isNotEmpty) {
return ParsedAddress.fetchTwitterAddress(
address: addressFromBio,
name: text,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name);
} else {
final domain = extractUnstoppableDomain(twitterUser.description);
if (domain.isNotEmpty) {
final parsedAddressFromDomain = await resolve(context, domain, currency);
if (parsedAddressFromDomain.addresses.isNotEmpty &&
parsedAddressFromDomain.addresses.first != domain) {
return ParsedAddress(
addresses: parsedAddressFromDomain.addresses,
name: text,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name,
parseFrom: ParseFrom.twitter,
);
}
}
}
final addressFromLocation = extractAddressByType(
raw: twitterUser.location,
type: CryptoCurrency.fromString(ticker,
walletCurrency: wallet.currency), requireSurroundingWhitespaces: false);
if (addressFromLocation != null && addressFromLocation.isNotEmpty) {
return ParsedAddress.fetchTwitterAddress(
address: addressFromLocation,
name: text,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name);
} else {
final domain = extractUnstoppableDomain(twitterUser.location);
if (domain.isNotEmpty) {
final parsedAddressFromDomain = await resolve(context, domain, currency);
if (parsedAddressFromDomain.addresses.isNotEmpty &&
parsedAddressFromDomain.addresses.first != domain) {
return ParsedAddress(
addresses: parsedAddressFromDomain.addresses,
name: text,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name,
parseFrom: ParseFrom.twitter,
);
}
}
}
final pinnedTweet = twitterUser.pinnedTweet?.text;
if (pinnedTweet != null) {
final addressFromPinnedTweet = extractAddressByType(
raw: pinnedTweet,
type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency),
requireSurroundingWhitespaces: true);
if (addressFromPinnedTweet != null) {
return ParsedAddress.fetchTwitterAddress(
address: addressFromPinnedTweet,
name: text,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name);
} else {
final domain = extractUnstoppableDomain(pinnedTweet);
if (domain.isNotEmpty) {
final parsedAddressFromDomain = await resolve(context, domain, currency);
if (parsedAddressFromDomain.addresses.isNotEmpty &&
parsedAddressFromDomain.addresses.first != domain) {
return ParsedAddress(
addresses: parsedAddressFromDomain.addresses,
name: text,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name,
parseFrom: ParseFrom.twitter,
);
}
}
}
}
}
}
// Mastodon example: @username@hostname.xxx
if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) {
if (settingsStore.lookupsMastodon) {
final subText = text.substring(1);
final hostNameIndex = subText.indexOf('@');
final hostName = subText.substring(hostNameIndex + 1);
final userName = subText.substring(0, hostNameIndex);
final mastodonUser =
await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName);
if (mastodonUser != null) {
final mastodonNote = _stripHtmlTags(mastodonUser.note);
String? addressFromBio = extractAddressByType(raw: mastodonNote, type: currency, requireSurroundingWhitespaces: true);
if (addressFromBio != null && addressFromBio.isNotEmpty) {
return ParsedAddress.fetchMastodonAddress(
address: addressFromBio,
name: text,
profileImageUrl: mastodonUser.profileImageUrl,
profileName: mastodonUser.username);
} else {
final pinnedPosts =
await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName);
if (pinnedPosts.isNotEmpty) {
final userPinnedPostsText = pinnedPosts.map((item) => _stripHtmlTags(item.content)).join('\n');
String? addressFromPinnedPost =
extractAddressByType(raw: userPinnedPostsText, type: currency,
requireSurroundingWhitespaces: true);
if (addressFromPinnedPost != null && addressFromPinnedPost.isNotEmpty) {
return ParsedAddress.fetchMastodonAddress(
address: addressFromPinnedPost,
name: text,
profileImageUrl: mastodonUser.profileImageUrl,
profileName: mastodonUser.username);
}
}
}
}
}
}
// .well-known scheme:
if (text.contains('.') && text.contains('@')) {
if (settingsStore.lookupsWellKnown) {
final record =
await WellKnownRecord.fetchAddressAndName(formattedName: text, currency: currency);
if (record != null) {
return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text);
}
}
if (walletType == WalletType.bitcoin) {
final record =
await LNUrlPayRecord.fetchAddressAndName(formattedName: text, currency: currency);
if (record != null) {
return ParsedAddress.fetchLNUrlPayAddress(address: record.address, name: text);
}
}
}
if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) {
final bool isFioRegistered = await FioAddressProvider.checkAvail(text);
if (isFioRegistered) {
final address = await FioAddressProvider.getPubAddress(text, ticker);
return ParsedAddress.fetchFioAddress(address: address, name: text);
}
}
if (text.hasOnlyEmojis) {
if (settingsStore.lookupsYatService) {
if (walletType != WalletType.haven) {
final addresses = await yatService.fetchYatAddress(text, ticker);
return ParsedAddress.fetchEmojiAddress(addresses: addresses, name: text);
}
}
}
final isNormalAddress = extractAddressByType(raw: text, type: currency)?.isNotEmpty ?? false;
if (text.length <= 30 && !isNormalAddress) {
final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text);
if (thorChainAddress != null && thorChainAddress.isNotEmpty) {
String? address =
thorChainAddress[ticker] ?? (ticker == 'RUNE' ? thorChainAddress['THOR'] : null);
if (address != null) {
return ParsedAddress.thorChainAddress(address: address, name: text);
}
}
}
final formattedName = OpenaliasRecord.formatDomainName(text);
final domainParts = formattedName.split('.');
final name = domainParts.last;
if (domainParts.length <= 1 || domainParts.first.isEmpty || name.isEmpty) {
return ParsedAddress(addresses: [text]);
}
if (unstoppableDomains.any((domain) => name.trim() == domain)) {
if (settingsStore.lookupsUnstoppableDomains) {
final address = await fetchUnstoppableDomainAddress(text, ticker);
if (address.isNotEmpty) {
return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text);
}
}
}
final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, ticker);
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap);
if (chosenAddress != null) {
try {
final dnsProof = await Bip353Record.fetchDnsProof(text);
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text, dnsProof: dnsProof);
} catch (e) {
printV('Bip353Record.fetchBip353AddressAddress error: $e');
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
}
}
}
final lowerText = text.toLowerCase();
if (lowerText.endsWith(".zec") || lowerText.endsWith(".zcash")) {
if (settingsStore.lookupsZcashNames) {
final address = await ZcashNamesRecord.fetchZcashNamesAddress(text);
if (address != null && address.isNotEmpty) {
return ParsedAddress.zcashNameAddress(address: address, name: text);
}
}
}
if (text.endsWith(".eth")) {
if (settingsStore.lookupsENS) {
final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet);
if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") {
return ParsedAddress.fetchEnsAddress(name: text, address: address);
}
}
}
if (formattedName.contains(".")) {
if (settingsStore.lookupsOpenAlias) {
final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName);
if (txtRecord != null) {
final record = await OpenaliasRecord.fetchAddressAndName(
formattedName: formattedName, ticker: ticker.toLowerCase(), txtRecord: txtRecord);
return ParsedAddress.fetchOpenAliasAddress(record: record, name: text);
}
}
}
if (isEmailFormat(text)) {
final nostrProfile = await NostrProfileHandler.queryProfile(context, text);
if (nostrProfile?.relays != null) {
final nostrUserData =
await NostrProfileHandler.processRelays(context, nostrProfile!, text);
if (nostrUserData != null) {
String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency);
if (addressFromBio != null && addressFromBio.isNotEmpty) {
return ParsedAddress.nostrAddress(
address: addressFromBio,
name: text,
profileImageUrl: nostrUserData.picture,
profileName: nostrUserData.name);
}
}
}
}
} catch (e) {
printV(e.toString());
}
return ParsedAddress(addresses: [text]);
}
Future<String?> _fetchZcashAddress(String handle) async {
final url = Uri.parse('https://zcash.me/$handle');
try {
final response = await ProxyWrapper().get(clearnetUri: url);
if (response.statusCode == 200) {
final addressRegex = RegExp(
r'(t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300})',
caseSensitive: true,
);
final match = addressRegex.firstMatch(response.body);
if (match != null) {
return match.group(0);
}
}
} catch (e) {
printV('Error fetching zcash.me profile: $e');
}
return null;
}
}
-212
View File
@@ -1,212 +0,0 @@
import 'package:cake_wallet/entities/openalias_record.dart';
import 'package:cake_wallet/entities/yat_record.dart';
enum ParseFrom {
unstoppableDomains,
openAlias,
yatRecord,
fio,
notParsed,
twitter,
ens,
contact,
mastodon,
nostr,
thorChain,
wellKnown,
zanoAlias,
zcashAddress,
zcashName,
bip353,
lnurlpay,
}
class ParsedAddress {
ParsedAddress({
required this.addresses,
this.name = '',
this.description = '',
this.profileImageUrl = '',
this.profileName = '',
this.parseFrom = ParseFrom.notParsed,
this.bip353DnsProof,
});
factory ParsedAddress.fetchEmojiAddress({
List<YatRecord>? addresses,
required String name,
}) {
if (addresses?.isEmpty ?? true) {
return ParsedAddress(addresses: [name], parseFrom: ParseFrom.yatRecord);
}
return ParsedAddress(
addresses: addresses!.map((e) => e.address).toList(),
name: name,
parseFrom: ParseFrom.yatRecord,
);
}
factory ParsedAddress.fetchUnstoppableDomainAddress({
String? address,
required String name,
}) {
if (address?.isEmpty ?? true) {
return ParsedAddress(addresses: [name]);
}
return ParsedAddress(
addresses: [address!],
name: name,
parseFrom: ParseFrom.unstoppableDomains,
);
}
factory ParsedAddress.fetchBip353AddressAddress ({
required String address,
required String name,
String? dnsProof,
}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.bip353,
bip353DnsProof: dnsProof,
);
}
factory ParsedAddress.fetchOpenAliasAddress(
{required OpenaliasRecord record, required String name}) {
if (record.address.isEmpty) {
return ParsedAddress(addresses: [name]);
}
return ParsedAddress(
addresses: [record.address],
name: record.name,
description: record.description,
parseFrom: ParseFrom.openAlias,
);
}
factory ParsedAddress.fetchFioAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.fio,
);
}
factory ParsedAddress.fetchTwitterAddress(
{required String address,
required String name,
required String profileImageUrl,
required String profileName,
String? description}) {
return ParsedAddress(
addresses: [address],
name: name,
description: description ?? '',
profileImageUrl: profileImageUrl,
profileName: profileName,
parseFrom: ParseFrom.twitter,
);
}
factory ParsedAddress.fetchMastodonAddress(
{required String address,
required String name,
required String profileImageUrl,
required String profileName}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.mastodon,
profileImageUrl: profileImageUrl,
profileName: profileName,
);
}
factory ParsedAddress.fetchContactAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.contact,
);
}
factory ParsedAddress.fetchEnsAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.ens,
);
}
factory ParsedAddress.nostrAddress(
{required String address,
required String name,
required String profileImageUrl,
required String profileName}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.nostr,
profileImageUrl: profileImageUrl,
profileName: profileName,
);
}
factory ParsedAddress.thorChainAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.thorChain,
);
}
factory ParsedAddress.zanoAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.zanoAlias,
);
}
factory ParsedAddress.zcashAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.zcashAddress,
);
}
factory ParsedAddress.zcashNameAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.zcashName,
);
}
factory ParsedAddress.fetchWellKnownAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.wellKnown,
);
}
factory ParsedAddress.fetchLNUrlPayAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.lnurlpay,
);
}
final List<String> addresses;
final String name;
final String description;
final String profileImageUrl;
final String profileName;
final ParseFrom parseFrom;
final String? bip353DnsProof;
}
+6
View File
@@ -102,7 +102,13 @@ class PreferencesKey {
static const lookupsOpenAlias = 'looks_up_open_alias';
static const lookupsENS = 'looks_up_ens';
static const lookupsZcashNames = 'looks_up_zcash_names';
static const lookupsZcashAddress = 'looks_up_zcash_address';
static const lookupsWellKnown = 'looks_up_well_known';
static const lookupsFio = 'looks_up_fio';
static const lookupsNostr = 'looks_up_nostr';
static const lookupsThorChain = 'looks_up_thor_chain';
static const lookupsBip353 = 'looks_up_bip353';
static const lookupsLNUrl = 'looks_up_lnurl';
static const useBlinkProtection = 'use_blink_protection';
static const usePayjoin = 'use_payjoin';
static const showPayjoinCard = 'show_payjoin_card';
@@ -1,30 +0,0 @@
import 'dart:convert';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
Future<String> fetchUnstoppableDomainAddress(String domain, String ticker) async {
var address = '';
try {
final uri = Uri.parse("https://api.unstoppabledomains.com/profile/public/${Uri.encodeQueryComponent(domain)}?fields=records");
final response = await ProxyWrapper().get(clearnetUri: uri);
final jsonParsed = json.decode(response.body) as Map<String, dynamic>;
if (jsonParsed["records"] == null) {
throw Exception(".records response from $uri is empty");
};
final records = jsonParsed["records"] as Map<String, dynamic>;
final key = "crypto.${ticker.toUpperCase()}.address";
if (records[key] == null) {
throw Exception(".records.${key} response from $uri is empty");
}
return records[key] as String? ?? '';
} catch (e) {
printV('Unstoppable domain error: ${e.toString()}');
address = '';
}
return address;
}
-93
View File
@@ -1,93 +0,0 @@
import 'dart:convert';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class WellKnownRecord {
WellKnownRecord({
required this.address,
required this.name,
});
final String name;
final String address;
static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency) async {
String jsonLocation = "";
switch (currency) {
case CryptoCurrency.nano:
jsonLocation = "nano-currency";
break;
// TODO: add other currencies
default:
return null;
}
// split the string by the @ symbol:
try {
final List<String> splitStrs = username.split("@");
String name = splitStrs.first.toLowerCase();
final String domain = splitStrs.last;
if (splitStrs.length == 3) {
// for username like @alice@domain.org instead of alice@domain.org
name = splitStrs[1];
}
if (name.isEmpty) {
name = "_";
}
// lookup domain/.well-known/nano-currency.json and check if it has a nano address:
final response = await ProxyWrapper().get(
clearnetUri: Uri.parse("https://$domain/.well-known/$jsonLocation.json?names=$name"),
headers: <String, String>{"Accept": "application/json"},
);
if (response.statusCode != 200) {
return null;
}
final Map<String, dynamic> decoded = json.decode(response.body) as Map<String, dynamic>;
// Access the first element in the names array and retrieve its address
final List<dynamic> names = decoded["names"] as List<dynamic>;
for (final dynamic item in names) {
if (item["name"].toLowerCase() == name) {
return item["address"] as String;
}
}
} catch (e) {
printV("error checking well-known username: $e");
}
return null;
}
static String formatDomainName(String name) {
String formattedName = name;
if (name.contains("@")) {
formattedName = name.replaceAll("@", ".");
}
return formattedName;
}
static Future<WellKnownRecord?> fetchAddressAndName({
required String formattedName,
required CryptoCurrency currency,
}) async {
String name = formattedName;
printV("formattedName: $formattedName");
final address = await checkWellKnownUsername(formattedName, currency);
if (address == null) {
return null;
}
return WellKnownRecord(address: address, name: name);
}
}
-13
View File
@@ -1,13 +0,0 @@
class YatRecord {
YatRecord({
required this.category,
required this.address,
});
YatRecord.fromJson(Map<String, dynamic> json)
: address = json['address'] as String,
category = json['category'] as String;
String category;
String address;
}
-29
View File
@@ -1,29 +0,0 @@
import 'dart:convert';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class ZanoAlias {
static Future<String?> fetchZanoAliasAddress(String alias) async {
try {
final uri = Uri.parse("http://37.27.100.59:10500/json_rpc");
final response = await ProxyWrapper().post(
clearnetUri: uri,
body: json.encode({
"id": 0,
"jsonrpc": "2.0",
"method": "get_alias_details",
"params": {"alias": alias}
}),
);
final jsonParsed = json.decode(response.body) as Map<String, dynamic>;
return jsonParsed['result']['alias_details']['address'] as String?;
} catch (e) {
printV('Zano Alias error: ${e.toString()}');
}
return null;
}
}
+52 -7
View File
@@ -50,6 +50,7 @@ import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
@@ -263,15 +264,34 @@ class _NewSendPageState extends State<NewSendPage> {
_addressFocusNode.addListener(() async {
if (!_addressFocusNode.hasFocus && _addressControllers[_selectedOutput].text.isNotEmpty) {
final output = widget.sendViewModel.outputs[_selectedOutput];
output.fetchParsedAddress(context).then((val) {
if (_addressControllers[_selectedOutput].text != output.extractedAddress) {
_addressControllers[_selectedOutput].text = output.extractedAddress;
}
});
await _resolveAddressForOutput(output);
}
});
}
Future<void> _resolveAddressForOutput(Output output) async {
final result = await widget.sendViewModel.resolveAddressForOutput(output);
if (result == null) return;
final confirmed = await showParsedAddressConfirmationAlert(context, result);
if (confirmed) {
output.applyAddressLookupResult(result);
} else {
output.resetParsedAddress();
return;
}
final outputIndex = widget.sendViewModel.outputs.indexOf(output);
if (outputIndex >= 0 &&
outputIndex < _addressControllers.length &&
output.extractedAddress.isNotEmpty &&
_addressControllers[outputIndex].text != output.extractedAddress) {
_addressControllers[outputIndex].text = output.extractedAddress;
}
}
@override
Widget build(BuildContext context) {
return Observer(builder: (_) {
@@ -377,7 +397,7 @@ class _NewSendPageState extends State<NewSendPage> {
focusNode: _addressFocusNode,
onURIScanned: (uri) async {
output.resetParsedAddress();
await output.fetchParsedAddress(context);
await _resolveAddressForOutput(output);
// Process the payment through the new flow
await _handlePaymentFlow(
@@ -404,7 +424,7 @@ class _NewSendPageState extends State<NewSendPage> {
_justHandledPasteButton = true;
try {
output.resetParsedAddress();
await output.fetchParsedAddress(context);
await _resolveAddressForOutput(output);
final address = output.isParsedAddress
? output.extractedAddress
@@ -1429,3 +1449,28 @@ class SendHelpPage extends StatelessWidget {
);
}
}
Future<bool> showParsedAddressConfirmationAlert(
BuildContext context,
ParsedAddress parsedAddress,
) async {
final confirmed = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).address_detected,
headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName,
headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty
? parsedAddress.addressSource.iconPath
: parsedAddress.profileImageUrl,
alertContent: S.of(context).extracted_address_content(
'${parsedAddress.handle} (${parsedAddress.addressSource.label})',
),
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(true),
);
},
);
return confirmed ?? false;
}
+8 -5
View File
@@ -1,8 +1,8 @@
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/exchange/exchange_trade_state.dart';
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
@@ -23,7 +23,6 @@ import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -52,7 +51,7 @@ import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class NewSwapPage extends StatefulWidget {
NewSwapPage(this.exchangeViewModel, this.authService, this.initialPaymentRequest,
NewSwapPage(this.exchangeViewModel, this.authService,this.adrResService, this.initialPaymentRequest,
{required this.walletSwitcherViewModel, CryptoCurrency? initialCurrency}) {
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
? exchangeViewModel.wallet.name
@@ -68,6 +67,7 @@ class NewSwapPage extends StatefulWidget {
final ExchangeViewModel exchangeViewModel;
final WalletSwitcherViewModel walletSwitcherViewModel;
final AuthService authService;
final AddressResolverService adrResService;
final PaymentRequest? initialPaymentRequest;
late final String? depositWalletName;
late final String? receiveWalletName;
@@ -423,8 +423,11 @@ class _NewSwapPageState extends State<NewSwapPage> {
Future<String> fetchParsedAddress(
BuildContext context, String domain, CryptoCurrency currency) async {
printV("$domain");
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
return extractAddressFromParsed(context, parsedAddress);
final parsedAddress = await widget.adrResService.resolve(
query: domain,
wallet: widget.exchangeViewModel.wallet,
currency: currency);
return parsedAddress.isNotEmpty ? parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? '' : '';
}
void _showFeeAlert(BuildContext context) async {
@@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/lnurlpay_record.dart';
import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurlpay_record.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
@@ -227,13 +227,14 @@ class SendTransactionDetails extends StatelessWidget {
decimals: 2)
: sendViewModel.pendingTransactionFeeFiatAmountFormatted;
final showAddress = !sendViewModel.outputs.any((e) =>
RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) ||
RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) ||
(e.isParsedAddress &&
e.parsedAddress.addresses.isNotEmpty &&
RegExp(AddressValidator.lnurlMatcher)
.hasMatch(e.parsedAddress.addresses.firstOrNull?.toLowerCase() ?? "")));
final showAddress = !sendViewModel.outputs.any((e) =>
RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) ||
RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) ||
(e.isParsedAddress &&
e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency] != null &&
e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency]!.isNotEmpty &&
RegExp(AddressValidator.lnurlMatcher)
.hasMatch(e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency]!.toLowerCase())));
final outputs = sendViewModel.outputs;
-150
View File
@@ -1,150 +0,0 @@
import 'dart:convert';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/nostr/nostr_user.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
import 'package:nostr_tools/nostr_tools.dart';
class NostrProfileHandler {
static final relayToDomainMap = {
'relay.snort.social': 'snort.social',
};
static Nip05 _nip05 = Nip05();
static Future<ProfilePointer?> queryProfile(BuildContext context, String nip05Address) async {
var profile = await _nip05.queryProfile(nip05Address);
if (profile?.pubkey != null) {
if (profile?.relays?.isNotEmpty == true) {
return profile;
} else {
await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relays_message);
}
}
return null;
}
static Future<UserMetadata?> processRelays(
BuildContext context,
ProfilePointer profile,
String nip05Address,
) async {
String userDomain = _extractDomain(nip05Address);
const int metaData = 0;
for (String relayUrl in profile.relays ?? []) {
final relayDomain = _getDomainFromRelayUrl(relayUrl);
final formattedRelayDomain = relayToDomainMap[relayDomain] ?? relayDomain;
if (formattedRelayDomain == userDomain) {
final userDomainData = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaData]);
if (userDomainData != null) {
return userDomainData;
}
}
}
await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relay_on_domain);
String? chosenRelayUrl =
await _showRelayChoiceDialog(context, profile.relays ?? []);
if (chosenRelayUrl != null) {
final userData = await _fetchInfoFromRelay(chosenRelayUrl, profile.pubkey, [metaData]);
if (userData != null) {
return userData;
}
}
return null;
}
static Future<UserMetadata?> _fetchInfoFromRelay(
String relayUrl, String userPubKey, List<int> kinds) async {
try {
final relay = RelayApi(relayUrl: relayUrl);
final stream = await relay.connect();
relay.sub([
Filter(
kinds: kinds,
authors: [userPubKey],
)
]);
await for (var message in stream) {
if (message.type == 'EVENT') {
final event = message.message as Event;
final eventContent = json.decode(event.content) as Map<String, dynamic>;
final userMetadata = UserMetadata.fromJson(eventContent);
relay.close();
return userMetadata;
}
}
relay.close();
return null;
} catch (e) {
printV('[!] Error with relay $relayUrl: $e');
return null;
}
}
static Future<void> _showErrorDialog(
BuildContext context, String title, String errorMessage) async {
if (context.mounted) {
await showPopUp<void>(
context: context,
builder: (BuildContext dialogContext) {
return AlertWithOneAction(
alertTitle: title,
alertContent: errorMessage,
buttonText: S.of(dialogContext).ok,
buttonAction: () => Navigator.of(dialogContext).pop(),
);
},
);
}
}
static String _extractDomain(String nip05Address) {
var parts = nip05Address.split('@');
return parts.length == 2 ? parts[1] : '';
}
static String _getDomainFromRelayUrl(String relayUrl) {
try {
var uri = Uri.parse(relayUrl);
return uri.host;
} catch (e) {
printV('Error parsing URL: $e');
return '';
}
}
static Future<String?> _showRelayChoiceDialog(
BuildContext context,
List<String> relays,
) async {
String? selectedRelay;
if (context.mounted) {
await showPopUp<void>(
context: context,
builder: (BuildContext dialogContext) {
return Picker<String>(
selectedAtIndex: 0,
title: S.of(dialogContext).choose_relay,
items: relays,
onItemSelected: (String relay) => selectedRelay = relay,
);
},
);
}
return selectedRelay;
}
}
+30 -12
View File
@@ -1,7 +1,7 @@
import 'package:cake_wallet/buy/sell_buy_states.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
@@ -16,7 +16,6 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -28,9 +27,10 @@ import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
class BuySellPage extends BasePage {
BuySellPage(this.buySellViewModel);
BuySellPage(this.buySellViewModel, this._resolver);
final BuySellViewModel buySellViewModel;
final AddressResolverService _resolver;
final cryptoCurrencyKey = GlobalKey<ExchangeCardState>();
final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
final _formKey = GlobalKey<FormState>();
@@ -300,7 +300,7 @@ class BuySellPage extends BasePage {
});
reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) {
if (cryptoAddressController != address) {
if (cryptoAddressController.text != address) {
cryptoCurrencyKey.currentState!.addressController.text = address;
}
});
@@ -320,8 +320,10 @@ class BuySellPage extends BasePage {
_cryptoAddressFocus.addListener(() async {
if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) {
final domain = cryptoAddressController.text;
buySellViewModel.cryptoCurrencyAddress = await fetchParsedAddress(
context, domain, buySellViewModel.cryptoCurrency);
final parsed = await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
if (parsed.isNotEmpty) {
buySellViewModel.cryptoCurrencyAddress = parsed;
}
}
});
@@ -410,8 +412,23 @@ class BuySellPage extends BasePage {
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context).colorScheme.surfaceContainerHighest,
borderColor: Theme.of(context).colorScheme.outlineVariant,
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
onPushPasteButton: (context) async {
final domain = cryptoCurrencyKey.currentState!.addressController.text;
final parsed =
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
if (parsed.isNotEmpty) {
buySellViewModel.cryptoCurrencyAddress = parsed;
}
},
onPushAddressBookButton: (context) async {
final domain = cryptoCurrencyKey.currentState!.addressController.text;
final parsed =
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
if (parsed.isNotEmpty) {
buySellViewModel.cryptoCurrencyAddress = parsed;
}
},
fillColor: buySellViewModel.isBuyAction
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.surfaceContainerLow,
@@ -537,9 +554,10 @@ class BuySellPage extends BasePage {
String domain,
CryptoCurrency currency,
) async {
final parsedAddress =
await getIt.get<AddressResolver>().resolve(context, domain, currency);
final address = await extractAddressFromParsed(context, parsedAddress);
return address;
final parsedAddresses = await _resolver.resolve(
query: domain, wallet: buySellViewModel.wallet, currency: currency);
return parsedAddresses.isNotEmpty
? parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? ''
: '';
}
}
@@ -340,6 +340,7 @@ class _ContactListBodyState extends State<ContactListBody> {
updateFunction: widget.contactListViewModel.reorderAccordingToContactList,
canReorder: widget.contactListViewModel.isEditable,
shrinkWrap: true,
padding: const EdgeInsets.only(bottom: 250),
itemBuilder: (context, index) {
final contact = contacts[index];
final contactContent =
+48 -4
View File
@@ -1,3 +1,4 @@
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -27,7 +28,15 @@ class ContactPage extends BasePage {
_nameController.text = contactViewModel.name;
_addressController.text = contactViewModel.address;
_nameController.addListener(() => contactViewModel.name = _nameController.text);
_addressController.addListener(() => contactViewModel.address = _addressController.text);
_addressController.addListener(() {
final address = _addressController.text;
if (address != contactViewModel.address) {
contactViewModel.displayName = '';
}
contactViewModel.address = address;
});
autorun((_) => _currencyTypeController.text = "${contactViewModel.currency?.toString() ?? ""} ${contactViewModel.currency?.tag != null ? "(${contactViewModel.currency?.tag})" : ""}");
}
@@ -98,7 +107,8 @@ class ContactPage extends BasePage {
focusNode: addressFocusNode,
displayName: contactViewModel.displayName,
addressController: _addressController,
onEditingComplete: ()=> contactViewModel.extractParsedAddress(context),
onEditingComplete: () {},
onPushPasteButton: (context) => _extractParsedAddress(context),
validator: AddressValidator(type: contactViewModel.currency!),
selectedCurrency: contactViewModel.currency!,
);
@@ -129,8 +139,7 @@ class ContactPage extends BasePage {
builder: (_) => PrimaryButton(
onPressed: () async {
FocusScope.of(context).unfocus();
await contactViewModel.extractParsedAddress(context);
_extractParsedAddress(context);
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
@@ -150,6 +159,16 @@ class ContactPage extends BasePage {
);
}
Future<void> _extractParsedAddress(BuildContext context) async {
final parsedAddress = await contactViewModel.extractParsedAddress(context);
if (parsedAddress == null) return;
final confirmed = await showParsedAddressConfirmationAlert(context, parsedAddress);
if (!confirmed) return;
contactViewModel.applyParsedAddress(parsedAddress);
}
void _presentCurrencyPicker(BuildContext context) {
showPopUp<void>(
builder: (_) => CurrencyPicker(
@@ -203,4 +222,29 @@ class ContactPage extends BasePage {
}
});
}
Future<bool> showParsedAddressConfirmationAlert(
BuildContext context,
ParsedAddress parsedAddress,
) async {
final confirmed = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).address_detected,
headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName,
headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty
? parsedAddress.addressSource.iconPath
: parsedAddress.profileImageUrl,
alertContent: S.of(context).extracted_address_content(
'${parsedAddress.handle} (${parsedAddress.addressSource.label})',
),
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(true),
);
},
);
return confirmed ?? false;
}
}
+5 -5
View File
@@ -1,3 +1,4 @@
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
@@ -11,8 +12,6 @@ import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -41,7 +40,7 @@ import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
class ExchangePage extends BasePage {
ExchangePage(this.exchangeViewModel, this.authService, this.initialPaymentRequest) {
ExchangePage(this.exchangeViewModel, this.authService, this.adrResService, this.initialPaymentRequest) {
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
? exchangeViewModel.wallet.name
: null;
@@ -52,6 +51,7 @@ class ExchangePage extends BasePage {
final ExchangeViewModel exchangeViewModel;
final AuthService authService;
final AddressResolverService adrResService;
final PaymentRequest? initialPaymentRequest;
final depositKey = GlobalKey<ExchangeCardState>();
final receiveKey = GlobalKey<ExchangeCardState>();
@@ -655,8 +655,8 @@ class ExchangePage extends BasePage {
Future<String> fetchParsedAddress(
BuildContext context, String domain, CryptoCurrency currency) async {
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
return extractAddressFromParsed(context, parsedAddress);
final parsedAddresses = await adrResService.resolve(query: domain, wallet:exchangeViewModel.wallet, currency: currency);
return parsedAddresses.isNotEmpty ? parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? '' : '';
}
void _showFeeAlert(BuildContext context) async {
-1
View File
@@ -780,7 +780,6 @@ class SendPage extends BasePage {
}
output.resetParsedAddress();
await output.fetchParsedAddress(context);
}
Output _defineCurrentOutput() {
@@ -1,150 +0,0 @@
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'choose_yat_address_alert.dart';
Future<String> extractAddressFromParsed(
BuildContext? context,
ParsedAddress parsedAddress) async {
if (context == null || !context.mounted) return parsedAddress.addresses.first;
var title = '';
var content = '';
var address = '';
var profileImageUrl = '';
var profileName = '';
switch (parsedAddress.parseFrom) {
case ParseFrom.unstoppableDomains:
title = S.of(context).address_detected;
content = S.of(context).address_from_domain(parsedAddress.name);
address = parsedAddress.addresses.first;
break;
case ParseFrom.ens:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (ENS)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.openAlias:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (OpenAlias)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.wellKnown:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Well-Known)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.fio:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (FIO)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.twitter:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)');
address = parsedAddress.addresses.first;
profileImageUrl = parsedAddress.profileImageUrl;
profileName = parsedAddress.profileName;
break;
case ParseFrom.mastodon:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)');
address = parsedAddress.addresses.first;
profileImageUrl = parsedAddress.profileImageUrl;
profileName = parsedAddress.profileName;
break;
case ParseFrom.nostr:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Nostr NIP-05)');
address = parsedAddress.addresses.first;
profileImageUrl = parsedAddress.profileImageUrl;
profileName = parsedAddress.profileName;
break;
case ParseFrom.thorChain:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (ThorChain)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.zanoAlias:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Zano Alias)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.zcashAddress:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Zcash.me)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.zcashName:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Zcash Names)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.bip353:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.lnurlpay:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Lightning)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.yatRecord:
if (parsedAddress.name.isEmpty) {
title = S.of(context).yat_error;
content = S.of(context).yat_error_content;
address = parsedAddress.addresses.first;
break;
}
title = S.of(context).address_detected;
content = S.of(context).address_from_yat(parsedAddress.name);
if (parsedAddress.addresses.length == 1) {
address = parsedAddress.addresses.first;
break;
}
content += S.of(context).choose_address;
address = await showPopUp<String?>(
context: context,
builder: (context) => PopScope(
child: ChooseYatAddressAlert(
alertTitle: title,
alertContent: content,
addresses: parsedAddress.addresses,
),
canPop: false,
),
) ??
'';
if (address.isEmpty) {
return parsedAddress.name;
}
return address;
case ParseFrom.contact:
case ParseFrom.notParsed:
return parsedAddress.addresses.first;
}
await showPopUp<void>(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: title,
headerTitleText: profileName.isEmpty ? null : profileName,
headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl,
alertContent: content,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
),
);
return address;
}
@@ -594,7 +594,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
controller: addressController,
onURIScanned: (uri) async {
output.resetParsedAddress();
await output.fetchParsedAddress(context);
// Process the payment through the new flow
await _handlePaymentFlow(
@@ -620,7 +619,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
_justHandledPasteButton = true;
try {
output.resetParsedAddress();
await output.fetchParsedAddress(context);
final address =
output.isParsedAddress ? output.extractedAddress : output.address;
@@ -643,7 +641,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
output.resetParsedAddress();
},
onSelectedContact: (contact) {
output.loadContact(contact);
},
validator: validator,
selectedCurrency: sendViewModel.selectedCryptoCurrency,
@@ -1033,8 +1030,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
if (current.isEmpty) return;
if (_justHandledPasteButton || _lastHandledAddress == current) return;
await output.fetchParsedAddress(context);
// If it's a URI with params, go through URI flow
if (current.contains('=')) {
try {
@@ -92,11 +92,9 @@ class SendTemplateCard extends StatelessWidget {
],
onPushPasteButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
onPushAddressBookButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest,
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
@@ -1,6 +1,7 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/settings/connection_sync_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -20,47 +21,21 @@ class DomainLookupsPage extends BasePage {
return Container(
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
SettingsSwitcherCell(
title: 'Twitter',
value: _connectionsSyncViewModel.lookupTwitter,
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsTwitter(value)),
SettingsSwitcherCell(
title: 'Mastodon',
value: _connectionsSyncViewModel.looksUpMastodon,
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsMastodon(value)),
SettingsSwitcherCell(
title: 'Yat service',
value: _connectionsSyncViewModel.looksUpYatService,
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsYatService(value)),
SettingsSwitcherCell(
title: 'Unstoppable Domains',
value: _connectionsSyncViewModel.looksUpUnstoppableDomains,
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsUnstoppableDomains(value)),
SettingsSwitcherCell(
title: 'OpenAlias',
value: _connectionsSyncViewModel.looksUpOpenAlias,
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsOpenAlias(value)),
SettingsSwitcherCell(
title: 'Ethereum Name Service',
value: _connectionsSyncViewModel.looksUpENS,
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsENS(value)),
SettingsSwitcherCell(
title: 'Zcash Names',
value: _connectionsSyncViewModel.lookupsZcashNames,
onValueChange: (_, bool value) =>
_connectionsSyncViewModel.setLookupsZcashNames(value)),
SettingsSwitcherCell(
title: '.well-known',
value: _connectionsSyncViewModel.looksUpWellKnown,
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsWellKnown(value)),
SettingsSwitcherCell(
title: 'Zano Aliases',
value: _connectionsSyncViewModel.lookupsZanoAlias,
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsZanoAlias(value)),
//if (!isHaven) it does not work correctly
],
children: _connectionsSyncViewModel.domainLookupSources
.map(
(source) => SettingsSwitcherCell(
title: source.label,
leading: source.iconPath.isNotEmpty ? CakeImageWidget(
imageUrl: source.iconPath,
width: 24,
height: 24,
) : SizedBox(width: 24, height: 24),
value: _connectionsSyncViewModel.lookupValue(source),
onValueChange: (_, bool value) =>
_connectionsSyncViewModel.setLookupValue(source, value),
),
)
.toList(),
),
);
}),
@@ -10,6 +10,7 @@ class FilteredList extends StatefulWidget {
this.canReorder = true,
this.shrinkWrap = false,
this.physics,
this.padding,
});
final ObservableList<dynamic> list;
@@ -18,6 +19,7 @@ class FilteredList extends StatefulWidget {
final bool canReorder;
final bool shrinkWrap;
final ScrollPhysics? physics;
final EdgeInsets? padding;
@override
FilteredListState createState() => FilteredListState();
@@ -48,6 +50,7 @@ class FilteredListState extends State<FilteredList> {
),
shrinkWrap: widget.shrinkWrap,
physics: widget.physics ?? const BouncingScrollPhysics(),
padding: widget.padding,
itemBuilder: widget.itemBuilder,
itemCount: widget.list.length,
onReorder: (int oldIndex, int newIndex) {
+1 -1
View File
@@ -1,10 +1,10 @@
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/src/screens/yat/widgets/first_introduction.dart';
import 'package:cake_wallet/src/screens/yat/widgets/second_introduction.dart';
import 'package:cake_wallet/src/screens/yat/widgets/third_introduction.dart';
import 'package:cake_wallet/src/screens/yat/widgets/yat_close_button.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
import 'package:animate_do/animate_do.dart';
+2 -4
View File
@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/section_divider.dart';
import 'package:flutter/material.dart';
@@ -184,10 +185,7 @@ class BaseAlertDialog extends StatelessWidget {
radius: 50,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
child: ClipOval(
child: Image.network(
imageUrl,
fit: BoxFit.cover,
),
child: CakeImageWidget (imageUrl: imageUrl, width: 100, height: 100, fit: BoxFit.cover),
),
),
);
@@ -163,7 +163,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
itemBuilder: (context, index) {
final isBatchSending = outputs.length > 1;
final item = outputs[index];
final contactName = item.parsedAddress.name;
final contactName = item.parsedAddress.profileName;
final isCakePayName = contactName == 'Cake Pay';
final batchContactTitle =
'${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}';
+67 -2
View File
@@ -132,7 +132,13 @@ abstract class SettingsStoreBase with Store {
required this.lookupsOpenAlias,
required this.lookupsENS,
required this.lookupsZcashNames,
required this.lookupsZcashAddress,
required this.lookupsWellKnown,
required this.lookupsFio,
required this.lookupsNostr,
required this.lookupsThorChain,
required this.lookupsBip353,
required this.lookupsLNUrl,
required this.usePayjoin,
required this.showPayjoinCard,
required this.customBitcoinFeeRate,
@@ -567,11 +573,41 @@ abstract class SettingsStoreBase with Store {
(bool looksUpZcashNames) => _sharedPreferences.setBool(
PreferencesKey.lookupsZcashNames, looksUpZcashNames));
reaction(
(_) => lookupsZcashAddress,
(bool lookupsZcashAddress) => _sharedPreferences.setBool(
PreferencesKey.lookupsZcashAddress, lookupsZcashAddress));
reaction(
(_) => lookupsWellKnown,
(bool looksUpWellKnown) =>
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
reaction(
(_) => lookupsFio,
(bool lookupsFio) =>
_sharedPreferences.setBool(PreferencesKey.lookupsFio, lookupsFio));
reaction(
(_) => lookupsNostr,
(bool lookupsNostr) =>
_sharedPreferences.setBool(PreferencesKey.lookupsNostr, lookupsNostr));
reaction(
(_) => lookupsThorChain,
(bool lookupsThorChain) =>
_sharedPreferences.setBool(PreferencesKey.lookupsThorChain, lookupsThorChain));
reaction(
(_) => lookupsBip353,
(bool lookupsBip353) =>
_sharedPreferences.setBool(PreferencesKey.lookupsBip353, lookupsBip353));
reaction(
(_) => lookupsLNUrl,
(bool lookupsLNUrl) =>
_sharedPreferences.setBool(PreferencesKey.lookupsLNUrl, lookupsLNUrl));
reaction((_) => usePayjoin,
(bool usePayjoin) => _sharedPreferences.setBool(PreferencesKey.usePayjoin, usePayjoin));
@@ -962,9 +998,27 @@ abstract class SettingsStoreBase with Store {
@observable
bool lookupsZcashNames;
@observable
bool lookupsZcashAddress;
@observable
bool lookupsWellKnown;
@observable
bool lookupsFio;
@observable
bool lookupsNostr;
@observable
bool lookupsThorChain;
@observable
bool lookupsBip353;
@observable
bool lookupsLNUrl;
@observable
bool usePayjoin;
@@ -1268,9 +1322,14 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true;
final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
final lookupsZcashNames =
sharedPreferences.getBool(PreferencesKey.lookupsZcashNames) ?? true;
final lookupsZcashNames = sharedPreferences.getBool(PreferencesKey.lookupsZcashNames) ?? true;
final lookupsZcashAddress = sharedPreferences.getBool(PreferencesKey.lookupsZcashAddress) ?? true;
final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
final lookupsFio = sharedPreferences.getBool(PreferencesKey.lookupsFio) ?? true;
final lookupsNostr = sharedPreferences.getBool(PreferencesKey.lookupsNostr) ?? true;
final lookupsThorChain = sharedPreferences.getBool(PreferencesKey.lookupsThorChain) ?? false;
final lookupsBip353 = sharedPreferences.getBool(PreferencesKey.lookupsBip353) ?? true;
final lookupsLNUrl = sharedPreferences.getBool(PreferencesKey.lookupsLNUrl) ?? true;
final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false;
final showPayjoinCard = sharedPreferences.getBool(PreferencesKey.showPayjoinCard) ?? true;
final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
@@ -1659,7 +1718,13 @@ abstract class SettingsStoreBase with Store {
lookupsOpenAlias: lookupsOpenAlias,
lookupsENS: lookupsENS,
lookupsZcashNames: lookupsZcashNames,
lookupsZcashAddress: lookupsZcashAddress,
lookupsWellKnown: lookupsWellKnown,
lookupsFio: lookupsFio,
lookupsNostr: lookupsNostr,
lookupsThorChain: lookupsThorChain,
lookupsBip353: lookupsBip353,
lookupsLNUrl: lookupsLNUrl,
usePayjoin: usePayjoin,
showPayjoinCard: showPayjoinCard,
customBitcoinFeeRate: customBitcoinFeeRate,
-10
View File
@@ -1,10 +0,0 @@
import 'package:flutter/foundation.dart';
class YatException implements Exception {
YatException({required this.text});
final String text;
@override
String toString() => 'Yat exception: $text';
}
@@ -1,7 +1,10 @@
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
@@ -16,7 +19,8 @@ part 'contact_view_model.g.dart';
class ContactViewModel = ContactViewModelBase with _$ContactViewModel;
abstract class ContactViewModelBase with Store {
ContactViewModelBase(this._contacts, {ContactRecord? contact})
ContactViewModelBase(this._contacts, this.appStore,this.adrResService,
{ContactRecord? contact})
: state = InitialExecutionState(),
currencies = CryptoCurrency.all,
_contact = contact,
@@ -26,6 +30,9 @@ abstract class ContactViewModelBase with Store {
currency = contact?.type,
lastChange = contact?.lastChange;
final AppStore appStore;
final AddressResolverService adrResService;
@observable
ExecutionState state;
@@ -58,16 +65,52 @@ abstract class ContactViewModelBase with Store {
void reset() {
address = '';
name = '';
displayName = '';
currency = null;
}
Future<void> extractParsedAddress(BuildContext context) async{
if(currency == null) return;
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, address, currency!);
if(parsedAddress.name.isNotEmpty) {
displayName = parsedAddress.name;
Future<ParsedAddress?> extractParsedAddress(BuildContext context) async {
final wallet = appStore.wallet;
final currentCurrency = currency;
final query = address.trim();
if (wallet == null) return null;
if (currentCurrency == null) return null;
if (query.isEmpty) return null;
// Check if the address is valid for the current currency (except for Zano, which can use handles as addresses)
if (currentCurrency != CryptoCurrency.zano) {
final isValidAddress = AddressValidator(type: currentCurrency).isValid(query);
if (isValidAddress) return null;
}
address = await extractAddressFromParsed(null, parsedAddress);
final parsedAddresses = await adrResService.resolve(
query: query,
wallet: wallet,
currency: currentCurrency,
);
if (parsedAddresses.isEmpty) return null;
final resolvedAddress = parsedAddresses.first.parsedAddressByCurrencyMap[currentCurrency];
if (resolvedAddress == null || resolvedAddress.isEmpty) return null;
return parsedAddresses.first;
}
@action
void applyParsedAddress(ParsedAddress parsedAddress) {
final currentCurrency = currency;
if (currentCurrency == null) return;
final resolvedAddress = parsedAddress.parsedAddressByCurrencyMap[currentCurrency];
if (resolvedAddress == null || resolvedAddress.isEmpty) return;
address = resolvedAddress;
displayName = parsedAddress.profileName.isNotEmpty
? parsedAddress.profileName
: parsedAddress.handle;
}
Future<void> save() async {
@@ -4,6 +4,7 @@ import 'dart:io' show Platform;
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/core/key_service.dart';
import "package:cw_core/balance_card_style_settings.dart";
import 'package:cake_wallet/core/trade_monitor.dart';
@@ -35,7 +36,6 @@ import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:cake_wallet/core/seed_validator.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
@@ -87,7 +87,7 @@ class WalletRestoreFromQRCode {
static String? _extractAddressFromUrl(String rawString, WalletType type) {
try {
return AddressResolver.extractAddressByType(
return AddressResolverUtils.extractAddressByType(
raw: rawString,
type: walletTypeToCryptoCurrency(
type,
+36 -29
View File
@@ -1,16 +1,16 @@
import 'dart:math' show min;
import 'package:cake_wallet/bitcoin/bitcoin.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/decred/decred.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@@ -37,7 +37,8 @@ class Output = OutputBase with _$Output;
abstract class OutputBase with Store {
OutputBase(this._wallet, this._appStore, this._fiatConversationStore, this.cryptoCurrencyHandler)
: key = UniqueKey(),
:
key = UniqueKey(),
sendAll = false,
cryptoAmount = '',
cryptoFullBalance = '',
@@ -47,7 +48,7 @@ abstract class OutputBase with Store {
memo = "",
extractedAddress = '',
estimatedFee = Money.zero(cryptoCurrencyHandler()),
parsedAddress = ParsedAddress(addresses: []) {
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) {
autorun((_) {
final status = _wallet.syncStatus;
printV("Sync status changed to $status. Recalculating fees");
@@ -56,6 +57,7 @@ abstract class OutputBase with Store {
});
}
Key key;
bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler());
@@ -83,7 +85,7 @@ abstract class OutputBase with Store {
@observable
String note;
@observable
String memo;
@@ -99,7 +101,7 @@ abstract class OutputBase with Store {
@computed
bool get isParsedAddress =>
parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty;
parsedAddress.addressSource != AddressSource.notParsed && parsedAddress.handle.isNotEmpty;
String roundedCryptoAmount(int digits) => displayCryptoAmount.withMaxDecimals(digits);
@@ -272,10 +274,22 @@ abstract class OutputBase with Store {
resetParsedAddress();
}
@action
void resetParsedAddress() {
displayName = null;
extractedAddress = '';
parsedAddress = ParsedAddress(addresses: []);
note = '';
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {});
}
@action
void applyAddressLookupResult(ParsedAddress result) {
final currency = cryptoCurrencyHandler();
parsedAddress = result;
extractedAddress = result.parsedAddressByCurrencyMap[currency] ?? '';
note = result.description;
displayName = result.profileName.isNotEmpty ? result.profileName : result.handle;
}
@action
@@ -334,33 +348,26 @@ abstract class OutputBase with Store {
Map<String, dynamic> get extra {
final fields = <String, dynamic>{};
if (parsedAddress.parseFrom == ParseFrom.bip353) {
fields['bip353_name'] = parsedAddress.name;
if (parsedAddress.addressSource == AddressSource.bip353) {
fields['bip353_name'] = parsedAddress.handle;
fields['bip353_proof'] = parsedAddress.bip353DnsProof;
}
return fields;
}
Future<void> fetchParsedAddress(BuildContext context) async {
final domain = address;
final currency = cryptoCurrencyHandler();
parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
if(parsedAddress.name.isNotEmpty) {
displayName = parsedAddress.name;
}
if (parsedAddress.parseFrom == ParseFrom.lnurlpay) {
cryptoCurrencyHandler(CryptoCurrency.btcln);
}
extractedAddress = await extractAddressFromParsed(context, parsedAddress);
note = parsedAddress.description;
}
@action
void loadContact(ContactBase contact) {
address = contact.name;
parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name);
extractedAddress = parsedAddress.addresses.first;
note = parsedAddress.description;
final currency = cryptoCurrencyHandler();
address = contact.address;
applyAddressLookupResult(
ParsedAddress(
parsedAddressByCurrencyMap: {currency: contact.address},
addressSource: AddressSource.contact,
handle: contact.name,
profileName: contact.name,
),
);
}
}
+38 -12
View File
@@ -1,6 +1,9 @@
import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/amount_validator.dart';
@@ -17,7 +20,6 @@ import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
@@ -103,6 +105,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
this._appStore,
this.sendTemplateViewModel,
this._fiatConversationStore,
this._adrResService,
this.balanceViewModel,
this.contactListViewModel,
this.transactionDescriptionBox,
@@ -182,6 +185,26 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
if (isBatchSending) outputs.remove(output);
}
Future<ParsedAddress?> resolveAddressForOutput(Output output) async {
final query = output.address.trim();
if (query.isEmpty) return null;
// Check if the address is valid for the current currency (except for Zano, which can use handles as addresses)
if (selectedCryptoCurrency != CryptoCurrency.zano) {
final isValidAddress = AddressValidator(type: selectedCryptoCurrency).isValid(query);
if (isValidAddress) return null;
}
final results = await _adrResService.resolve(
query: query,
wallet: wallet,
currency: selectedCryptoCurrency,
);
return results.isEmpty ? null : results.first;
}
@action
void clearOutputs() {
outputs.clear();
@@ -383,14 +406,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// If silent payments scanning, can still send payments
(wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus);
bool isSendToSilentPayments(Output output) =>
wallet.type == WalletType.bitcoin &&
(RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(output.address) ||
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
.hasMatch(output.extractedAddress) ||
(output.parsedAddress.addresses.isNotEmpty &&
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
.hasMatch(output.parsedAddress.addresses[0])));
bool isSendToSilentPayments(Output output) {
if (wallet.type != WalletType.bitcoin) return false;
final sp = RegExp(AddressValidator.silentPaymentAddressPatternMainnet);
final address = output.extractedAddress.isNotEmpty
? output.extractedAddress
: output.address;
return sp.hasMatch(address);
}
@computed
List<Template> get templates => sendTemplateViewModel.templates
@@ -454,6 +480,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final HardwareWalletViewModel? hardwareWalletViewModel;
final FeesViewModel feesViewModel;
final FiatConversionStore _fiatConversationStore;
final AddressResolverService _adrResService;
final Box<TransactionDescription> transactionDescriptionBox;
@computed
@@ -606,8 +633,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
clearOutputs();
outputs.first.address = paymentRequest.address;
outputs.first.parsedAddress =
ParsedAddress(addresses: [paymentRequest.address], name: ocpRequest!.receiverName);
outputs.first.parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {currency:paymentRequest.address}, handle: ocpRequest!.receiverName);
outputs.first.setCryptoAmount(paymentRequest.amount);
outputs.first.note = ocpRequest!.receiverName;
@@ -1342,7 +1368,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
for (final output in outputs) {
final address =
output.isParsedAddress ? output.parsedAddress.addresses.first : output.address;
output.isParsedAddress ? output.parsedAddress.parsedAddressByCurrencyMap[selectedCryptoCurrency] ?? '' : output.address;
if (address.isNotEmpty &&
!contactAddresses.contains(address) &&
@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/evm/evm.dart';
@@ -55,6 +57,24 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
@computed
bool get lookupsNostr => _settingsStore.lookupsNostr;
@computed
bool get lookupsFio => _settingsStore.lookupsFio;
@computed
bool get lookupsBip353 => _settingsStore.lookupsBip353;
@computed
bool get lookupsThorChain => _settingsStore.lookupsThorChain;
@computed
bool get lookupsZcashAddress => _settingsStore.lookupsZcashAddress;
@computed
bool get lookupsLNUrlPay => _settingsStore.lookupsLNUrl;
@computed
ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus;
@@ -68,6 +88,99 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get builtinTor => _settingsStore.currentBuiltinTor;
List<AddressSource> get domainLookupSources => AddressResolverService.supportedSources;
bool lookupValue(AddressSource source) {
switch (source) {
case AddressSource.twitter:
return lookupTwitter;
case AddressSource.mastodon:
return looksUpMastodon;
case AddressSource.yatRecord:
return looksUpYatService;
case AddressSource.unstoppableDomains:
return looksUpUnstoppableDomains;
case AddressSource.openAlias:
return looksUpOpenAlias;
case AddressSource.ens:
return looksUpENS;
case AddressSource.zcashName:
return lookupsZcashNames;
case AddressSource.zcashAddress:
return lookupsZcashAddress;
case AddressSource.wellKnown:
return looksUpWellKnown;
case AddressSource.zanoAlias:
return lookupsZanoAlias;
case AddressSource.bip353:
return lookupsBip353;
case AddressSource.fio:
return lookupsFio;
case AddressSource.lnurlPay:
return lookupsLNUrlPay;
case AddressSource.thorChain:
return lookupsThorChain;
case AddressSource.nostr:
return lookupsNostr;
case AddressSource.contact:
case AddressSource.notParsed:
return false;
}
}
@action
void setLookupValue(AddressSource source, bool value) {
switch (source) {
case AddressSource.twitter:
_settingsStore.lookupsTwitter = value;
break;
case AddressSource.mastodon:
_settingsStore.lookupsMastodon = value;
break;
case AddressSource.yatRecord:
_settingsStore.lookupsYatService = value;
break;
case AddressSource.unstoppableDomains:
_settingsStore.lookupsUnstoppableDomains = value;
break;
case AddressSource.openAlias:
_settingsStore.lookupsOpenAlias = value;
break;
case AddressSource.ens:
_settingsStore.lookupsENS = value;
break;
case AddressSource.zcashName:
_settingsStore.lookupsZcashNames = value;
break;
case AddressSource.zcashAddress:
_settingsStore.lookupsZcashAddress = value;
break;
case AddressSource.wellKnown:
_settingsStore.lookupsWellKnown = value;
break;
case AddressSource.zanoAlias:
_settingsStore.lookupsZanoAlias = value;
break;
case AddressSource.bip353:
_settingsStore.lookupsBip353 = value;
break;
case AddressSource.fio:
_settingsStore.lookupsFio = value;
break;
case AddressSource.lnurlPay:
_settingsStore.lookupsLNUrl = value;
break;
case AddressSource.thorChain:
_settingsStore.lookupsThorChain = value;
break;
case AddressSource.nostr:
_settingsStore.lookupsNostr = value;
break;
case AddressSource.contact:
case AddressSource.notParsed:
break;
}
}
@computed
bool get hasPowNodes => [WalletType.nano, WalletType.banano].contains(_wallet.type);
@@ -119,32 +232,6 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get canUseTronGrid => _wallet.type == WalletType.tron;
@action
void setLookupsTwitter(bool value) => _settingsStore.lookupsTwitter = value;
@action
void setLookupsZanoAlias(bool value) => _settingsStore.lookupsZanoAlias = value;
@action
void setLookupsMastodon(bool value) => _settingsStore.lookupsMastodon = value;
@action
void setLookupsENS(bool value) => _settingsStore.lookupsENS = value;
@action
void setLookupsZcashNames(bool value) => _settingsStore.lookupsZcashNames = value;
@action
void setLookupsWellKnown(bool value) => _settingsStore.lookupsWellKnown = value;
@action
void setLookupsYatService(bool value) => _settingsStore.lookupsYatService = value;
@action
void setLookupsUnstoppableDomains(bool value) => _settingsStore.lookupsUnstoppableDomains = value;
@action
void setLookupsOpenAlias(bool value) => _settingsStore.lookupsOpenAlias = value;
@action
void setUseMempoolFeeAPI(bool value) => _settingsStore.useMempoolFeeAPI = value;
@@ -2,6 +2,7 @@ import 'dart:core';
import 'dart:developer' as dev;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
@@ -17,7 +18,6 @@ import 'package:cake_wallet/reactions/wallet_utils.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/utils/list_item.dart';
import 'package:cake_wallet/utils/qr_util.dart';
+1
View File
@@ -295,6 +295,7 @@ flutter:
- assets/new-ui/crypto_full_icons/
- assets/new-ui/hardware_wallets/
- assets/new-ui/node_speed_badges/
- assets/new-ui/address_sources/
fonts:
- family: Lato
+4
View File
@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="#F7931A"/>
<path d="M74.5527 42.2225C75.6531 34.8671 70.0528 30.9131 62.3952 28.2753L64.8792 18.3116L58.8142 16.8001L56.3959 26.5013C54.8015 26.1039 53.1639 25.7291 51.5366 25.3577L53.9723 15.5926L47.9108 14.0811L45.4251 24.0414C44.1053 23.7408 42.8097 23.4438 41.5522 23.1311L41.5591 23.1L33.195 21.0115L31.5816 27.4893C31.5816 27.4893 36.0815 28.5206 35.9865 28.5845C38.4429 29.1977 38.8868 30.8232 38.8125 32.1119L35.983 43.4627C36.1523 43.5059 36.3717 43.5681 36.6135 43.6648C36.4114 43.6147 36.1955 43.5595 35.9727 43.5059L32.0065 59.4068C31.7059 60.153 30.9442 61.2724 29.2271 60.8474C29.2876 60.9355 24.8188 59.7471 24.8188 59.7471L21.8079 66.6896L29.7004 68.657C31.1687 69.025 32.6076 69.4102 34.0241 69.7729L31.5142 79.8507L37.5722 81.3622L40.058 71.3916C41.7129 71.8407 43.3194 72.2553 44.8913 72.6457L42.4142 82.5697L48.4791 84.0811L50.9891 74.0224C61.3311 75.9796 69.1079 75.1901 72.3814 65.8362C75.0191 58.3047 72.2501 53.9602 66.8087 51.1273C70.7714 50.2135 73.7564 47.6068 74.5527 42.2225ZM60.6954 61.6541C58.8211 69.1857 46.1402 65.1141 42.029 64.0932L45.3594 50.742C49.4707 51.7681 62.6543 53.7996 60.6954 61.6541ZM62.5714 42.1136C60.8612 48.9645 50.3067 45.4838 46.883 44.6305L49.9025 32.5213C53.3263 33.3747 64.3523 34.9673 62.5714 42.1136Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="#0080BC"/>
<g clip-path="url(#clip0_1796_8124)">
<path d="M48.295 13.5958L28.0864 46.8476C27.9279 47.1082 27.5597 47.1373 27.3631 46.9039C25.5841 44.7915 18.956 35.8046 27.1575 27.6136C34.6414 20.1394 44.1738 14.8104 47.7065 12.9685C48.1073 12.7596 48.5295 13.2098 48.295 13.5958Z" fill="white"/>
<path d="M47.1773 85.9954C47.5807 86.2778 48.0775 85.7964 47.8073 85.3851C43.2934 78.5197 28.2888 55.6765 26.216 52.2476C24.1716 48.8653 20.1506 43.2443 19.8152 38.4353C19.7817 37.9553 19.1178 37.8578 18.9508 38.3093C18.6815 39.0373 18.3948 39.9064 18.1275 40.8992C14.7539 53.4315 19.6535 66.7299 30.2942 74.1779L47.1773 85.9954Z" fill="white"/>
<path d="M49.9531 85.4041L70.1617 52.1524C70.3201 51.8918 70.6884 51.8627 70.885 52.0961C72.664 54.2083 79.292 63.1952 71.0906 71.3863C63.6067 78.8605 54.0744 84.1895 50.5415 86.0314C50.1408 86.2403 49.7186 85.7901 49.9531 85.4041Z" fill="white"/>
<path d="M51.0928 13.0001C50.6895 12.7178 50.1926 13.1992 50.4628 13.6103C54.9767 20.4758 69.9815 43.3189 72.054 46.7479C74.0986 50.1302 78.1195 55.7512 78.455 60.5601C78.4885 61.0403 79.1523 61.1378 79.3193 60.6863C79.5886 59.9581 79.8754 59.089 80.1427 58.0963C83.5162 45.5642 78.6167 32.2657 67.9759 24.8176L51.0928 13.0001Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1796_8124">
<rect width="65.8537" height="75" fill="white" transform="translate(17.0732 12)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+20
View File
@@ -0,0 +1,20 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="white"/>
<g clip-path="url(#clip0_1796_8061)">
<mask id="mask0_1796_8061" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="30" y="7" width="39" height="85">
<path d="M68.6254 7H30.3754V92H68.6254V7Z" fill="white"/>
</mask>
<g mask="url(#mask0_1796_8061)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.4602 62.2937V90.2068C50.4602 91.6904 49.5269 92.3988 48.3755 91.7711L32.5551 83.1131C31.2768 82.2526 30.4957 80.8197 30.4602 79.2804V52.0003C30.6935 52.1875 30.937 52.3491 31.1957 52.4911L50.4602 62.2937Z" fill="#765CD6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.4986 27.0001V28.6668C30.4875 28.0585 30.4875 27.5056 30.4986 27.0001Z" fill="#765CD6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.3823 8.91749V29.2234C66.3562 30.5088 65.679 31.5001 64.5952 32.1871L55.3363 37.0709L54.6596 37.2712L31.9748 25.8617C31.9435 25.8409 31.9123 25.8252 31.881 25.8096C31.7976 25.768 31.7143 25.742 31.6309 25.716C31.5736 25.7055 31.5163 25.6899 31.4642 25.6847C31.4121 25.6743 31.36 25.6743 31.3079 25.6743C31.1516 25.6795 31.0005 25.716 30.8598 25.7784C30.8129 25.8044 30.766 25.8252 30.7244 25.8565C30.7244 25.8565 30.7244 25.8617 30.7139 25.8617L30.6983 25.8773C31.3548 24.6856 32.6312 23.9986 34.5329 22.9787L64.5952 7.58006C65.5748 7.08048 66.3823 7.65812 66.3823 8.91749Z" fill="#765CD6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.4602 44.6319V64.8773C65.4602 66.1336 64.6748 66.7197 63.7221 66.2216L31.9956 49.553C31.276 49.0909 30.744 48.3955 30.4602 47.5959C30.4855 47.6375 30.5058 47.679 30.5362 47.7152C30.5565 47.7414 30.5768 47.7673 30.6021 47.7932C30.6072 47.8035 30.6122 47.8089 30.6224 47.8191C30.6426 47.8399 30.6629 47.8658 30.6882 47.8866C30.7136 47.9177 30.744 47.9385 30.7744 47.9644C30.82 47.9957 30.8605 48.0267 30.9112 48.0475C31.2102 48.2084 31.5902 48.1981 31.9956 47.9852L54.0587 36.3988L55.4358 37.0006L63.7221 41.4652C64.7811 42.1557 65.4349 43.3445 65.4602 44.6319Z" fill="#A08AEE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.4603 37.0071L32.3006 48.4954C31.8751 48.7065 31.4762 48.7168 31.1623 48.5573C31.1091 48.5367 31.0666 48.5057 31.0187 48.4749C30.9868 48.4492 30.9549 48.4286 30.9283 48.3978C30.9017 48.377 30.8804 48.3513 30.8591 48.3308C30.8485 48.3205 30.8432 48.3154 30.8379 48.3051C30.8113 48.2792 30.79 48.2535 30.7687 48.2278C30.7368 48.1918 30.7155 48.1507 30.6889 48.1094C30.5453 47.8623 30.4602 47.5434 30.4602 47.1725V26.8363C30.4602 26.2289 30.6677 25.776 30.9974 25.5341C31.0028 25.5289 31.0081 25.5238 31.0134 25.5238C31.0134 25.5238 31.0187 25.5135 31.024 25.5135C31.0666 25.4826 31.1145 25.462 31.1623 25.4362C31.3059 25.3745 31.4602 25.3385 31.6198 25.3333C31.673 25.3333 31.7262 25.3333 31.7794 25.3436C31.8325 25.3488 31.8911 25.3642 31.9496 25.3745C32.0347 25.4002 32.1198 25.426 32.2049 25.4671C32.2368 25.4825 32.2687 25.498 32.3006 25.5186L55.4603 37.0071Z" fill="#8B73E2"/>
</g>
</g>
<defs>
<clipPath id="clip0_1796_8061">
<rect width="38.5302" height="85" fill="white" transform="translate(30.2349 7)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" rx="4.8" fill="#F7931A"/>
<path d="M5.27657 12.4243L14.9284 4.10719C15.349 3.83785 15.7499 4.10719 15.4932 4.56924L12.4128 10.6274H17.9061C17.9061 10.6274 18.7789 10.6274 17.9061 11.3461L8.4083 19.7145C7.74089 20.2792 7.27883 19.9712 7.74089 19.0984L10.7186 13.1944H5.27657C5.27657 13.1944 4.4038 13.1944 5.27657 12.4243Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

@@ -0,0 +1,16 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" rx="4.8" fill="#0080BC"/>
<rect width="24" height="24" rx="4.8" fill="black"/>
<g clip-path="url(#clip0_11690_63602)">
<path d="M4.5918 20.4602L16.32 15.5214L12.6072 11.7565L4.5918 20.4602ZM8.90023 7.99751L12.613 11.7565L19.2405 3.66016L8.90023 7.99751Z" fill="url(#paint0_linear_11690_63602)"/>
</g>
<defs>
<linearGradient id="paint0_linear_11690_63602" x1="4.59205" y1="12.0603" x2="19.2384" y2="12.0603" gradientUnits="userSpaceOnUse">
<stop stop-color="#00CCFF"/>
<stop offset="1" stop-color="#33FF99"/>
</linearGradient>
<clipPath id="clip0_11690_63602">
<rect width="14.6487" height="16.8" fill="white" transform="translate(4.5918 3.66016)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 801 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="white"/>
<rect width="100" height="100" rx="20" fill="url(#paint0_linear_1796_8080)"/>
<path d="M48.9203 12C61.2483 12 66.5702 12.7569 67.975 12.9648C76.3007 14.1954 83.3244 20.5461 84.4086 28.6045C84.9984 34.5903 84.4871 44.3329 84.4838 45.9316C84.4837 46.4039 84.4153 50.6972 84.3881 51.1504C83.6583 62.6651 76.4779 67.2124 68.933 68.6611C68.8308 68.6918 68.7114 68.7125 68.5922 68.7363C63.8088 69.6704 58.6844 69.9193 53.8226 70.0557C52.66 70.0864 51.5007 70.0859 50.3383 70.0859C45.5042 70.0872 40.6867 69.5154 35.9877 68.3818C35.9629 68.3755 35.9365 68.3744 35.9115 68.3799C35.8865 68.3853 35.863 68.3969 35.8431 68.4131C35.8232 68.4293 35.8076 68.4503 35.7972 68.4736C35.7869 68.4972 35.7822 68.5232 35.7836 68.5488C35.9164 70.0785 36.2484 71.5848 36.7719 73.0283C37.4231 74.6988 39.6979 78.7099 48.1527 78.71C53.0659 78.719 57.9628 78.1472 62.7416 77.0059C62.7658 77.0003 62.7916 77.0004 62.8158 77.0059C62.84 77.0113 62.8627 77.0219 62.8822 77.0371C62.9018 77.0525 62.918 77.0725 62.9291 77.0947C62.94 77.117 62.9462 77.1413 62.9467 77.166V82.8184C62.9457 82.8449 62.9385 82.871 62.9261 82.8945C62.9138 82.918 62.8958 82.9382 62.8744 82.9541C61.3129 84.0858 59.1891 84.7306 57.3754 85.3066C56.5754 85.5589 55.7651 85.7799 54.9447 85.9707C47.4862 87.6693 39.7015 87.2592 32.4633 84.7852C25.7024 82.4126 18.8017 76.5964 17.097 69.6084C16.1867 65.8248 15.5448 61.9816 15.1771 58.1074C14.6453 52.275 14.6009 46.4286 14.3724 40.5723C14.2122 36.4887 14.3048 32.037 15.1674 28.0215C16.9607 19.8676 24.3518 14.1613 32.4457 12.9648C33.8504 12.7569 36.496 12 48.8275 12H48.9203ZM60.8353 24.2715C56.7818 24.2716 53.7139 25.8332 51.6683 28.9521L49.6908 32.2656L47.7162 28.9521C45.6705 25.8331 42.602 24.2715 38.5414 24.2715C35.0333 24.2716 32.2069 25.506 30.0521 27.9092C27.9635 30.318 26.9193 33.5677 26.9193 37.6582V57.6777H34.849V38.248C34.849 34.1574 36.5711 32.0713 40.0179 32.0713C43.8297 32.0713 45.7426 34.5394 45.7426 39.4141V50.0488H53.6283V39.4141C53.6283 34.5395 55.5374 32.0714 59.349 32.0713C62.8164 32.0713 64.5219 34.1574 64.5219 38.248V57.6777H72.4554V37.6582C72.4578 33.5655 71.415 30.3157 69.3285 27.9092C67.167 25.5059 64.3403 24.2715 60.8353 24.2715Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_1796_8080" x1="50" y1="0" x2="50" y2="100" gradientUnits="userSpaceOnUse">
<stop stop-color="#6364FF"/>
<stop offset="1" stop-color="#563ACC"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="white"/>
<path d="M39.1444 22.0044C45.185 17.9363 51.8829 16.7852 59.0741 17.0318C71.0322 17.4427 80.6067 25.6207 82.4559 37.4966C82.949 40.5785 83.1961 43.7427 83.2372 46.8657C83.3194 57.1388 83.2782 67.412 83.2782 77.6851C83.2782 81.2601 82.5386 82.0005 78.9637 82.0005H54.1024C51.0206 82.0005 50.3622 81.3428 50.3622 78.3023C50.3211 71.6041 50.2805 64.9055 51.6776 58.2896C52.9104 52.5777 55.7457 50.1121 61.6219 49.9888C65.4025 49.9066 69.1422 50.1122 72.9227 49.9068C74.4843 49.8246 76.1691 49.2901 77.5663 48.5093C79.3333 47.5641 79.4562 45.7558 78.7987 43.9888C78.1823 42.3456 76.8678 42.3452 75.3065 42.3452C72.5122 42.3863 69.6759 42.1813 66.9637 41.606C64.3752 41.0717 62.6085 39.3044 62.5262 36.4282C62.403 32.8532 60.3484 30.6749 57.2255 29.3189C53.3627 27.634 49.2938 27.2638 45.1844 27.4693C42.1848 27.6336 39.226 28.0865 36.2675 28.5796C32.3227 29.2372 30.1037 31.5381 30.4735 35.4829C30.9666 40.7017 31.9529 45.8796 33.0624 50.9751C35.0348 60.0155 36.72 69.0563 36.5145 78.3433C36.4734 81.1783 35.6923 82.0004 32.8573 82.0005H21.1454C18.5155 82.0005 17.6113 81.0962 17.6112 78.3843C17.529 68.8508 17.5292 59.3171 17.5292 49.8247V21.8814C17.5292 18.635 18.2692 17.854 21.4745 17.854C25.0906 17.854 28.7481 17.8951 32.3641 17.854C34.2131 17.813 35.2814 18.5937 35.6512 20.4839C36.0211 22.4152 37.4596 23.1139 39.1444 22.0044ZM53.0341 33.2222C55.7459 33.2224 58.1706 35.606 58.1708 38.3179C58.2119 41.0711 55.8276 43.4137 52.91 43.3726C50.1159 43.3313 47.8973 41.0711 47.9384 38.2769C47.9796 35.5237 50.2809 33.2222 53.0341 33.2222Z" fill="#9544DC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,13 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="black"/>
<path d="M53.7579 9.88531C73.8474 12.0444 89.5589 29.0155 89.4991 49.7047C89.4991 71.7734 71.6287 89.7044 49.5001 89.7047C27.4313 89.7047 9.50012 71.8335 9.50012 49.7047C9.50036 29.0754 25.152 12.0443 45.3615 9.88531L43.3224 16.8414C27.8505 19.78 16.1566 33.3932 16.1564 49.7047C16.1564 68.1154 31.149 83.108 49.5597 83.108C67.9704 83.1079 82.963 68.1153 82.963 49.7047C82.9628 33.3931 71.2681 19.7799 55.796 16.8414L53.7579 9.88531ZM61.0743 48.2662C61.7941 50.9646 64.2528 52.8835 68.8702 51.6841L79.1847 46.4664L52.6779 73.4527L49.5001 5.6275L61.0743 48.2662ZM46.381 73.3931L19.8751 46.4664L30.1896 51.6841C34.8072 52.8835 37.2658 50.9644 37.9855 48.3257L49.5001 5.6275L46.381 73.3931Z" fill="#00C3FF"/>
<path d="M53.7579 9.88531C73.8474 12.0444 89.5589 29.0155 89.4991 49.7047C89.4991 71.7734 71.6287 89.7044 49.5001 89.7047C27.4313 89.7047 9.50012 71.8335 9.50012 49.7047C9.50036 29.0754 25.152 12.0443 45.3615 9.88531L43.3224 16.8414C27.8505 19.78 16.1566 33.3932 16.1564 49.7047C16.1564 68.1154 31.149 83.108 49.5597 83.108C67.9704 83.1079 82.963 68.1153 82.963 49.7047C82.9628 33.3931 71.2681 19.7799 55.796 16.8414L53.7579 9.88531ZM61.0743 48.2662C61.7941 50.9646 64.2528 52.8835 68.8702 51.6841L79.1847 46.4664L52.6779 73.4527L49.5001 5.6275L61.0743 48.2662ZM46.381 73.3931L19.8751 46.4664L30.1896 51.6841C34.8072 52.8835 37.2658 50.9644 37.9855 48.3257L49.5001 5.6275L46.381 73.3931Z" fill="url(#paint0_linear_1796_8073)"/>
<defs>
<linearGradient id="paint0_linear_1796_8073" x1="9.50012" y1="7.66632" x2="78.7647" y2="76.9306" gradientUnits="userSpaceOnUse">
<stop offset="0.180653" stop-color="#007EC4"/>
<stop offset="0.585847" stop-color="#00AEEF"/>
<stop offset="0.813526" stop-color="#43C9F4"/>
<stop offset="1" stop-color="#A3E1F9"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,5 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="#0D67FE"/>
<path d="M87.5 15.1819V42.417L7.5 74.598L87.5 15.1819Z" fill="#00C9FF"/>
<path d="M72.5025 14V60.9822C72.5025 67.5477 69.8684 73.8444 65.1797 78.487C60.491 83.1297 54.1317 85.7378 47.5009 85.7378C40.8702 85.7378 34.5109 83.1297 29.8222 78.487C25.1335 73.8444 22.4994 67.5477 22.4994 60.9822V41.1851L37.4977 33.0122V60.9822C37.3574 62.2028 37.479 63.4391 37.8546 64.6101C38.2304 65.7811 38.8516 66.8601 39.6778 67.7767C40.5041 68.6933 41.5165 69.4266 42.6489 69.9288C43.7814 70.431 45.0081 70.6907 46.249 70.6907C47.4897 70.6907 48.7166 70.431 49.849 69.9288C50.9813 69.4266 51.9939 68.6933 52.82 67.7767C53.6463 66.8601 54.2677 65.7811 54.6433 64.6101C55.0191 63.4391 55.1407 62.2028 55.0001 60.9822V23.4811L72.5025 14Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 899 B

@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="#7D7D7D"/>
<path d="M50 83.3333C45.4444 83.3333 41.1389 82.4583 37.0833 80.7083C33.0278 78.9583 29.4861 76.5694 26.4583 73.5417C23.4305 70.5139 21.0417 66.9722 19.2917 62.9167C17.5417 58.8611 16.6667 54.5556 16.6667 50C16.6667 45.3889 17.5417 41.0694 19.2917 37.0417C21.0417 33.0139 23.4305 29.4861 26.4583 26.4583C29.4861 23.4306 33.0278 21.0417 37.0833 19.2917C41.1389 17.5417 45.4444 16.6667 50 16.6667C54.6111 16.6667 58.9305 17.5417 62.9583 19.2917C66.9861 21.0417 70.5139 23.4306 73.5417 26.4583C76.5694 29.4861 78.9583 33.0139 80.7083 37.0417C82.4583 41.0694 83.3333 45.3889 83.3333 50C83.3333 54.5556 82.4583 58.8611 80.7083 62.9167C78.9583 66.9722 76.5694 70.5139 73.5417 73.5417C70.5139 76.5694 66.9861 78.9583 62.9583 80.7083C58.9305 82.4583 54.6111 83.3333 50 83.3333ZM50 76.5C51.4444 74.5 52.6944 72.4167 53.75 70.25C54.8055 68.0833 55.6667 65.7778 56.3333 63.3333H43.6667C44.3333 65.7778 45.1944 68.0833 46.25 70.25C47.3055 72.4167 48.5555 74.5 50 76.5ZM41.3333 75.1667C40.3333 73.3333 39.4583 71.4306 38.7083 69.4583C37.9583 67.4861 37.3333 65.4444 36.8333 63.3333H27C28.6111 66.1111 30.625 68.5278 33.0417 70.5833C35.4583 72.6389 38.2222 74.1667 41.3333 75.1667ZM58.6667 75.1667C61.7778 74.1667 64.5417 72.6389 66.9583 70.5833C69.375 68.5278 71.3889 66.1111 73 63.3333H63.1667C62.6667 65.4444 62.0417 67.4861 61.2917 69.4583C60.5417 71.4306 59.6667 73.3333 58.6667 75.1667ZM24.1667 56.6667H35.5C35.3333 55.5556 35.2083 54.4583 35.125 53.375C35.0417 52.2917 35 51.1667 35 50C35 48.8333 35.0417 47.7083 35.125 46.625C35.2083 45.5417 35.3333 44.4444 35.5 43.3333H24.1667C23.8889 44.4444 23.6805 45.5417 23.5417 46.625C23.4028 47.7083 23.3333 48.8333 23.3333 50C23.3333 51.1667 23.4028 52.2917 23.5417 53.375C23.6805 54.4583 23.8889 55.5556 24.1667 56.6667ZM42.1667 56.6667H57.8333C58 55.5556 58.125 54.4583 58.2083 53.375C58.2917 52.2917 58.3333 51.1667 58.3333 50C58.3333 48.8333 58.2917 47.7083 58.2083 46.625C58.125 45.5417 58 44.4444 57.8333 43.3333H42.1667C42 44.4444 41.875 45.5417 41.7917 46.625C41.7083 47.7083 41.6667 48.8333 41.6667 50C41.6667 51.1667 41.7083 52.2917 41.7917 53.375C41.875 54.4583 42 55.5556 42.1667 56.6667ZM64.5 56.6667H75.8333C76.1111 55.5556 76.3194 54.4583 76.4583 53.375C76.5972 52.2917 76.6667 51.1667 76.6667 50C76.6667 48.8333 76.5972 47.7083 76.4583 46.625C76.3194 45.5417 76.1111 44.4444 75.8333 43.3333H64.5C64.6667 44.4444 64.7917 45.5417 64.875 46.625C64.9583 47.7083 65 48.8333 65 50C65 51.1667 64.9583 52.2917 64.875 53.375C64.7917 54.4583 64.6667 55.5556 64.5 56.6667ZM63.1667 36.6667H73C71.3889 33.8889 69.375 31.4722 66.9583 29.4167C64.5417 27.3611 61.7778 25.8333 58.6667 24.8333C59.6667 26.6667 60.5417 28.5694 61.2917 30.5417C62.0417 32.5139 62.6667 34.5556 63.1667 36.6667ZM43.6667 36.6667H56.3333C55.6667 34.2222 54.8055 31.9167 53.75 29.75C52.6944 27.5833 51.4444 25.5 50 23.5C48.5555 25.5 47.3055 27.5833 46.25 29.75C45.1944 31.9167 44.3333 34.2222 43.6667 36.6667ZM27 36.6667H36.8333C37.3333 34.5556 37.9583 32.5139 38.7083 30.5417C39.4583 28.5694 40.3333 26.6667 41.3333 24.8333C38.2222 25.8333 35.4583 27.3611 33.0417 29.4167C30.625 31.4722 28.6111 33.8889 27 36.6667Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="black"/>
<g clip-path="url(#clip0_1796_8041)">
<path d="M70.0666 17.8833H80.8L57.2333 44.7167L84.7666 81.1167H63.16L46.2433 58.9967L26.8766 81.1167H16.1433L41.11 52.4167L14.7433 17.8833H36.8866L52.17 38.09L70.0666 17.8833ZM66.31 74.8167H72.26L33.76 23.95H27.3666L66.31 74.8167Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1796_8041">
<rect width="70" height="63.2333" fill="white" transform="translate(15 17.8833)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 593 B

+11
View File
@@ -0,0 +1,11 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="#151327"/>
<g clip-path="url(#clip0_1796_8108)">
<path d="M46.7845 87.5C40.1522 87.5 34.1818 85.057 29.0489 80.2495C26.2123 77.5833 24.3212 74.1157 23.092 69.3214C22.3355 66.3662 22.0249 63.4501 21.8087 60.9809C21.5386 57.7758 21.2414 54.663 20.9848 52.036L20.9577 51.6944C20.7281 49.3694 20.485 46.9527 20.2013 44.6014C19.9177 42.3162 19.5935 39.9781 19.2693 37.732L18.9181 35.197C18.8506 34.7242 18.7695 34.2382 18.702 33.7653C18.5264 32.6489 18.3508 31.5455 18.2832 30.4159C18.0806 27.0272 20.1203 24.0981 23.4702 22.9422C24.6994 22.5219 25.9961 22.4299 27.2253 22.6401C27.3874 20.2364 28.6977 18.0429 30.8454 16.7294C33.5199 15.0876 36.8293 15.232 39.4768 17.0972C41.1112 18.2531 42.0973 19.8949 42.489 22.1147C43.1104 25.5692 43.7317 29.0105 44.3531 32.465L44.5422 33.4763L46.5954 23.5464C46.9195 21.9834 47.2436 20.3809 47.5543 18.7916C48.2703 15.0876 51.4041 12.5 55.1593 12.5C59.0359 12.5131 62.2912 15.324 62.7505 19.028C62.764 19.1068 62.764 19.1856 62.7776 19.2513C64.0067 19.0674 65.263 19.1594 66.4382 19.5797C70.2069 20.9063 72.2466 24.6497 71.4089 28.6953L70.6526 32.3205C69.707 36.7995 68.7614 41.331 67.8969 45.8231C69.1938 44.2074 70.9768 43.1568 72.9355 42.8678C74.9345 42.5788 76.9607 43.1043 78.6626 44.3389C81.1347 46.1384 82.2692 49.3169 81.4858 52.2592C80.7159 55.1358 79.3785 57.7233 78.0819 60.232C77.7441 60.8756 77.4199 61.5191 77.0957 62.176C74.2457 67.9028 71.3414 73.4326 67.0866 78.1873C63.0611 82.6928 58.7659 85.4511 53.957 86.6199C51.4987 87.198 49.0944 87.5 46.7845 87.5ZM25.9016 26.9615C25.5909 26.9615 25.2802 27.014 24.983 27.1191C23.5377 27.6182 22.7408 28.7215 22.8353 30.1401C22.8894 31.0727 23.0514 32.0578 23.2 33.0035C23.2946 33.5683 23.3756 34.0806 23.4432 34.5928L23.7944 37.1278C24.1185 39.4002 24.4427 41.7514 24.7399 44.0632C25.0371 46.4666 25.2802 48.9099 25.5099 51.2609L25.5369 51.6024C25.7935 54.2427 26.1042 57.3817 26.3743 60.5998C26.5635 62.8986 26.8606 65.5911 27.536 68.2311C28.5491 72.1847 30.0349 74.9692 32.2232 77.0314C37.991 82.443 44.9069 84.1899 52.836 82.2722C56.6991 81.3266 60.2247 79.0279 63.6285 75.232C67.5187 70.8843 70.2608 65.6436 72.9623 60.2058C73.3001 59.536 73.6379 58.8662 73.9889 58.2093C75.2316 55.8056 76.3933 53.5334 77.0553 51.1163C77.3795 49.9342 76.9067 48.6076 75.9205 47.8853C75.1912 47.36 74.3942 47.1367 73.6107 47.2547C72.8273 47.373 72.0845 47.8195 71.5035 48.5421L65.6411 59.2732C65.128 60.219 64.0067 60.6656 62.9533 60.3633C61.8995 60.0483 61.2242 59.0632 61.3324 58.0123C61.6834 54.6367 62.2373 51.0376 63.1289 46.3487C64.0609 41.3837 65.1144 36.366 66.141 31.5193L66.9245 27.8021C67.3162 25.9238 66.5192 24.3345 64.8848 23.7566C63.8582 23.3888 62.5751 23.704 61.7241 24.5184C61.0217 25.1883 59.968 25.3459 59.0899 24.9124C58.2121 24.479 57.7122 23.5596 57.8608 22.6138C57.8879 22.4693 57.9014 22.3117 57.9147 22.1672C57.9554 21.8389 58.0093 21.4843 58.0635 21.1559C58.1714 20.5517 58.266 19.9869 58.2121 19.5666C58.0229 18.0823 56.7262 16.9527 55.1729 16.9396C53.6059 16.9396 52.3633 18.0166 52.0526 19.606C51.7419 21.2084 51.4041 22.824 51.0799 24.4002L46.6359 45.9151C46.4198 46.9527 45.4742 47.7015 44.3936 47.7015C44.3801 47.7015 44.3801 47.7015 44.3666 47.7015C43.2725 47.6883 42.3269 46.9132 42.1378 45.8626C41.6516 43.1831 41.1653 40.4904 40.679 37.8109L39.8415 33.2137C39.2202 29.7592 38.5853 26.3047 37.9774 22.8503C37.7343 21.5105 37.2345 20.9983 36.7753 20.6699C35.6676 19.8818 34.3574 19.803 33.2768 20.4728C32.1691 21.1559 31.6018 22.4168 31.845 23.704C32.007 24.5447 32.1556 25.3984 32.2907 26.2522C32.4663 27.2373 31.9395 28.2355 30.994 28.6559C30.0484 29.0762 28.9543 28.8529 28.2789 28.0911C27.6441 27.3555 26.7796 26.9615 25.9016 26.9615Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1796_8108">
<rect width="75" height="75" fill="white" transform="translate(12.5 12.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+37
View File
@@ -0,0 +1,37 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="20" fill="#0C0C3A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.3609 17.0015H62.9359C65.5294 17.0015 67.7119 17.0015 69.4984 17.15C71.3674 17.3045 73.1479 17.642 74.8384 18.518C77.4239 19.8612 79.5179 21.9883 80.8204 24.5945C81.6829 26.3135 82.0144 28.121 82.1674 30.02C82.3129 31.835 82.3129 34.052 82.3129 36.6875V43.9865C82.3129 46.622 82.3129 48.839 82.1674 50.654C82.0144 52.553 81.6829 54.362 80.8204 56.0795C79.5182 58.6863 77.4241 60.8139 74.8384 62.1575C73.1479 63.032 71.3674 63.3695 69.4984 63.524C67.7119 63.674 65.5294 63.674 62.9344 63.674H40.3174C39.5362 63.6705 38.7736 63.4351 38.1263 62.9977C37.479 62.5603 36.9761 61.9405 36.6814 61.217C36.3802 60.4891 36.2975 59.6892 36.4435 58.9151C36.5894 58.141 36.9578 57.4262 37.5034 56.858L56.1799 37.5305L61.8094 43.145L49.7179 55.658H62.7769C65.5714 55.658 67.4299 55.655 68.8564 55.538C70.2364 55.4225 70.8634 55.22 71.2564 55.016C72.3522 54.4467 73.2396 53.5451 73.7914 52.4405C73.9924 52.0415 74.1919 51.4055 74.3044 50.0015C74.4214 48.5525 74.4244 46.664 74.4244 43.826V36.8465C74.4244 34.0085 74.4214 32.1215 74.3044 30.671C74.1919 29.2685 73.9924 28.631 73.7914 28.232C73.2396 27.1274 72.3522 26.2258 71.2564 25.6565C70.8634 25.4525 70.2364 25.2515 68.8564 25.136C67.4299 25.016 65.5714 25.0145 62.7769 25.0145H55.5214C52.6969 25.0145 50.8159 25.0175 49.3729 25.1375C47.9779 25.2545 47.3449 25.46 46.9504 25.667C45.8484 26.2449 44.9593 27.1592 44.4124 28.277C44.2129 28.6805 44.0194 29.324 43.9219 30.746C43.8199 32.213 43.8394 34.121 43.8739 36.992L43.9144 40.2875L36.0259 40.385L35.9854 36.9275C35.9524 34.262 35.9254 32.0225 36.0514 30.1865C36.1834 28.2695 36.4984 26.4395 37.3534 24.6995C38.6437 22.0611 40.7419 19.903 43.3429 18.539C45.0454 17.6495 46.8424 17.309 48.7279 17.15C50.5324 17 52.7389 17 55.3609 17V17.0015Z" fill="url(#paint0_linear_1796_8095)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.3609 17.0015H62.9359C65.5294 17.0015 67.7119 17.0015 69.4984 17.15C71.3674 17.3045 73.1479 17.642 74.8384 18.518C77.4239 19.8612 79.5179 21.9883 80.8204 24.5945C81.6829 26.3135 82.0144 28.121 82.1674 30.02C82.3129 31.835 82.3129 34.052 82.3129 36.6875V43.9865C82.3129 46.622 82.3129 48.839 82.1674 50.654C82.0144 52.553 81.6829 54.362 80.8204 56.0795C79.5182 58.6863 77.4241 60.8139 74.8384 62.1575C73.1479 63.032 71.3674 63.3695 69.4984 63.524C67.7119 63.674 65.5294 63.674 62.9344 63.674H40.3174C39.5362 63.6705 38.7736 63.4351 38.1263 62.9977C37.479 62.5603 36.9761 61.9405 36.6814 61.217C36.3802 60.4891 36.2975 59.6892 36.4435 58.9151C36.5894 58.141 36.9578 57.4262 37.5034 56.858L56.1799 37.5305L61.8094 43.145L49.7179 55.658H62.7769C65.5714 55.658 67.4299 55.655 68.8564 55.538C70.2364 55.4225 70.8634 55.22 71.2564 55.016C72.3522 54.4467 73.2396 53.5451 73.7914 52.4405C73.9924 52.0415 74.1919 51.4055 74.3044 50.0015C74.4214 48.5525 74.4244 46.664 74.4244 43.826V36.8465C74.4244 34.0085 74.4214 32.1215 74.3044 30.671C74.1919 29.2685 73.9924 28.631 73.7914 28.232C73.2396 27.1274 72.3522 26.2258 71.2564 25.6565C70.8634 25.4525 70.2364 25.2515 68.8564 25.136C67.4299 25.016 65.5714 25.0145 62.7769 25.0145H55.5214C52.6969 25.0145 50.8159 25.0175 49.3729 25.1375C47.9779 25.2545 47.3449 25.46 46.9504 25.667C45.8484 26.2449 44.9593 27.1592 44.4124 28.277C44.2129 28.6805 44.0194 29.324 43.9219 30.746C43.8199 32.213 43.8394 34.121 43.8739 36.992L43.9144 40.2875L36.0259 40.385L35.9854 36.9275C35.9524 34.262 35.9254 32.0225 36.0514 30.1865C36.1834 28.2695 36.4984 26.4395 37.3534 24.6995C38.6437 22.0611 40.7419 19.903 43.3429 18.539C45.0454 17.6495 46.8424 17.309 48.7279 17.15C50.5324 17 52.7389 17 55.3609 17V17.0015Z" fill="url(#paint1_radial_1796_8095)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.3609 17.0015H62.9359C65.5294 17.0015 67.7119 17.0015 69.4984 17.15C71.3674 17.3045 73.1479 17.642 74.8384 18.518C77.4239 19.8612 79.5179 21.9883 80.8204 24.5945C81.6829 26.3135 82.0144 28.121 82.1674 30.02C82.3129 31.835 82.3129 34.052 82.3129 36.6875V43.9865C82.3129 46.622 82.3129 48.839 82.1674 50.654C82.0144 52.553 81.6829 54.362 80.8204 56.0795C79.5182 58.6863 77.4241 60.8139 74.8384 62.1575C73.1479 63.032 71.3674 63.3695 69.4984 63.524C67.7119 63.674 65.5294 63.674 62.9344 63.674H40.3174C39.5362 63.6705 38.7736 63.4351 38.1263 62.9977C37.479 62.5603 36.9761 61.9405 36.6814 61.217C36.3802 60.4891 36.2975 59.6892 36.4435 58.9151C36.5894 58.141 36.9578 57.4262 37.5034 56.858L56.1799 37.5305L61.8094 43.145L49.7179 55.658H62.7769C65.5714 55.658 67.4299 55.655 68.8564 55.538C70.2364 55.4225 70.8634 55.22 71.2564 55.016C72.3522 54.4467 73.2396 53.5451 73.7914 52.4405C73.9924 52.0415 74.1919 51.4055 74.3044 50.0015C74.4214 48.5525 74.4244 46.664 74.4244 43.826V36.8465C74.4244 34.0085 74.4214 32.1215 74.3044 30.671C74.1919 29.2685 73.9924 28.631 73.7914 28.232C73.2396 27.1274 72.3522 26.2258 71.2564 25.6565C70.8634 25.4525 70.2364 25.2515 68.8564 25.136C67.4299 25.016 65.5714 25.0145 62.7769 25.0145H55.5214C52.6969 25.0145 50.8159 25.0175 49.3729 25.1375C47.9779 25.2545 47.3449 25.46 46.9504 25.667C45.8484 26.2449 44.9593 27.1592 44.4124 28.277C44.2129 28.6805 44.0194 29.324 43.9219 30.746C43.8199 32.213 43.8394 34.121 43.8739 36.992L43.9144 40.2875L36.0259 40.385L35.9854 36.9275C35.9524 34.262 35.9254 32.0225 36.0514 30.1865C36.1834 28.2695 36.4984 26.4395 37.3534 24.6995C38.6437 22.0611 40.7419 19.903 43.3429 18.539C45.0454 17.6495 46.8424 17.309 48.7279 17.15C50.5324 17 52.7389 17 55.3609 17V17.0015Z" fill="url(#paint2_radial_1796_8095)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.3609 17.0015H62.9359C65.5294 17.0015 67.7119 17.0015 69.4984 17.15C71.3674 17.3045 73.1479 17.642 74.8384 18.518C77.4239 19.8612 79.5179 21.9883 80.8204 24.5945C81.6829 26.3135 82.0144 28.121 82.1674 30.02C82.3129 31.835 82.3129 34.052 82.3129 36.6875V43.9865C82.3129 46.622 82.3129 48.839 82.1674 50.654C82.0144 52.553 81.6829 54.362 80.8204 56.0795C79.5182 58.6863 77.4241 60.8139 74.8384 62.1575C73.1479 63.032 71.3674 63.3695 69.4984 63.524C67.7119 63.674 65.5294 63.674 62.9344 63.674H40.3174C39.5362 63.6705 38.7736 63.4351 38.1263 62.9977C37.479 62.5603 36.9761 61.9405 36.6814 61.217C36.3802 60.4891 36.2975 59.6892 36.4435 58.9151C36.5894 58.141 36.9578 57.4262 37.5034 56.858L56.1799 37.5305L61.8094 43.145L49.7179 55.658H62.7769C65.5714 55.658 67.4299 55.655 68.8564 55.538C70.2364 55.4225 70.8634 55.22 71.2564 55.016C72.3522 54.4467 73.2396 53.5451 73.7914 52.4405C73.9924 52.0415 74.1919 51.4055 74.3044 50.0015C74.4214 48.5525 74.4244 46.664 74.4244 43.826V36.8465C74.4244 34.0085 74.4214 32.1215 74.3044 30.671C74.1919 29.2685 73.9924 28.631 73.7914 28.232C73.2396 27.1274 72.3522 26.2258 71.2564 25.6565C70.8634 25.4525 70.2364 25.2515 68.8564 25.136C67.4299 25.016 65.5714 25.0145 62.7769 25.0145H55.5214C52.6969 25.0145 50.8159 25.0175 49.3729 25.1375C47.9779 25.2545 47.3449 25.46 46.9504 25.667C45.8484 26.2449 44.9593 27.1592 44.4124 28.277C44.2129 28.6805 44.0194 29.324 43.9219 30.746C43.8199 32.213 43.8394 34.121 43.8739 36.992L43.9144 40.2875L36.0259 40.385L35.9854 36.9275C35.9524 34.262 35.9254 32.0225 36.0514 30.1865C36.1834 28.2695 36.4984 26.4395 37.3534 24.6995C38.6437 22.0611 40.7419 19.903 43.3429 18.539C45.0454 17.6495 46.8424 17.309 48.7279 17.15C50.5324 17 52.7389 17 55.3609 17V17.0015Z" fill="url(#paint3_radial_1796_8095)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.455 74.8655C31.883 74.984 33.74 74.987 36.5345 74.987H43.913C46.46 74.987 48.1535 74.984 49.4585 74.885C50.7245 74.789 51.3035 74.6195 51.668 74.45C52.9351 73.8565 53.9474 72.8282 54.521 71.552C54.686 71.183 54.854 70.592 54.9485 69.308C55.046 67.982 55.049 66.2615 55.049 63.6725H62.9375V63.8195C62.9375 66.2225 62.9375 68.2445 62.816 69.905C62.6885 71.639 62.411 73.2965 61.691 74.8925C60.3375 77.9061 57.9477 80.3342 54.956 81.7355C53.3855 82.4675 51.7535 82.748 50.0465 82.877C48.4115 83.0015 46.4225 83.0015 44.057 83.0015H36.377C33.782 83.0015 31.5995 83.0015 29.813 82.853C27.944 82.6985 26.1635 82.361 24.473 81.485C21.8875 80.1418 19.7935 78.0147 18.491 75.4085C17.6285 73.6895 17.297 71.882 17.144 69.9815C17 68.168 17 65.951 17 63.3155V56.0165C17 53.381 17 51.164 17.1455 49.349C17.2985 47.45 17.63 45.641 18.4925 43.9235C19.7948 41.3167 21.8888 39.1891 24.4745 37.8455C26.165 36.971 27.9455 36.6335 29.8145 36.4775C31.601 36.3305 33.7835 36.3305 36.3785 36.3305H58.9955C61.1735 36.3305 62.939 38.1245 62.939 40.337C62.939 42.5495 61.1735 44.345 58.994 44.345H36.536C33.7415 44.345 31.8845 44.348 30.4565 44.465C29.0765 44.5805 28.4495 44.783 28.0565 44.987C26.9607 45.5563 26.0733 46.4578 25.5215 47.5625C25.3205 47.9615 25.121 48.5975 25.0085 50.0015C24.8915 51.4505 24.8885 53.339 24.8885 56.177V63.155C24.8885 65.993 24.8915 67.88 25.0085 69.3305C25.121 70.733 25.3205 71.3705 25.5215 71.7695C26.0765 72.8795 26.9645 73.7795 28.0565 74.345C28.4495 74.549 29.075 74.75 30.455 74.8655Z" fill="url(#paint4_linear_1796_8095)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.455 74.8655C31.883 74.984 33.74 74.987 36.5345 74.987H43.913C46.46 74.987 48.1535 74.984 49.4585 74.885C50.7245 74.789 51.3035 74.6195 51.668 74.45C52.9351 73.8565 53.9474 72.8282 54.521 71.552C54.686 71.183 54.854 70.592 54.9485 69.308C55.046 67.982 55.049 66.2615 55.049 63.6725H62.9375V63.8195C62.9375 66.2225 62.9375 68.2445 62.816 69.905C62.6885 71.639 62.411 73.2965 61.691 74.8925C60.3375 77.9061 57.9477 80.3342 54.956 81.7355C53.3855 82.4675 51.7535 82.748 50.0465 82.877C48.4115 83.0015 46.4225 83.0015 44.057 83.0015H36.377C33.782 83.0015 31.5995 83.0015 29.813 82.853C27.944 82.6985 26.1635 82.361 24.473 81.485C21.8875 80.1418 19.7935 78.0147 18.491 75.4085C17.6285 73.6895 17.297 71.882 17.144 69.9815C17 68.168 17 65.951 17 63.3155V56.0165C17 53.381 17 51.164 17.1455 49.349C17.2985 47.45 17.63 45.641 18.4925 43.9235C19.7948 41.3167 21.8888 39.1891 24.4745 37.8455C26.165 36.971 27.9455 36.6335 29.8145 36.4775C31.601 36.3305 33.7835 36.3305 36.3785 36.3305H58.9955C61.1735 36.3305 62.939 38.1245 62.939 40.337C62.939 42.5495 61.1735 44.345 58.994 44.345H36.536C33.7415 44.345 31.8845 44.348 30.4565 44.465C29.0765 44.5805 28.4495 44.783 28.0565 44.987C26.9607 45.5563 26.0733 46.4578 25.5215 47.5625C25.3205 47.9615 25.121 48.5975 25.0085 50.0015C24.8915 51.4505 24.8885 53.339 24.8885 56.177V63.155C24.8885 65.993 24.8915 67.88 25.0085 69.3305C25.121 70.733 25.3205 71.3705 25.5215 71.7695C26.0765 72.8795 26.9645 73.7795 28.0565 74.345C28.4495 74.549 29.075 74.75 30.455 74.8655Z" fill="url(#paint5_radial_1796_8095)"/>
<defs>
<linearGradient id="paint0_linear_1796_8095" x1="48.1204" y1="63.311" x2="48.6544" y2="16.9415" gradientUnits="userSpaceOnUse">
<stop offset="0.431" stop-color="#498FFD"/>
<stop offset="1" stop-color="#16D1D6"/>
</linearGradient>
<radialGradient id="paint1_radial_1796_8095" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(35.41 31.1318) rotate(-15.65) scale(29.8203 29.9076)">
<stop stop-color="#18CFD7"/>
<stop offset="1" stop-color="#18CFD7" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint2_radial_1796_8095" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(52.8292 44.3816) rotate(152.23) scale(11.1721 11.1879)">
<stop stop-color="#4990FE"/>
<stop offset="0.354" stop-color="#4990FE"/>
<stop offset="1" stop-color="#4990FE" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint3_radial_1796_8095" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.1765 59.5247) rotate(-46.089) scale(20.3623 20.3268)">
<stop stop-color="#4990FE"/>
<stop offset="0.406" stop-color="#4990FE"/>
<stop offset="1" stop-color="#4990FE" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint4_linear_1796_8095" x1="63.182" y1="83.186" x2="63.353" y2="35.8655" gradientUnits="userSpaceOnUse">
<stop stop-color="#2950FF"/>
<stop offset="0.822" stop-color="#498FFD"/>
</linearGradient>
<radialGradient id="paint5_radial_1796_8095" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(63.182 63.311) rotate(129.142) scale(20.1345 19.9017)">
<stop offset="0.337" stop-color="#2950FF"/>
<stop offset="0.792" stop-color="#2950FF" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

@@ -1,4 +1,4 @@
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/core/address_resolver/parse_address_from_domain.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter_test/flutter_test.dart';