fix: bip-353 and fio

This commit is contained in:
Serhii 2025-06-24 17:22:53 +03:00
parent a8eb98b612
commit 1daea9bc01
5 changed files with 84 additions and 117 deletions

View file

@ -314,8 +314,8 @@ class AddressValidator extends TextValidator {
pattern = 'ban_[0-9a-zA-Z]{60}'; pattern = 'ban_[0-9a-zA-Z]{60}';
case CryptoCurrency.bch: case CryptoCurrency.bch:
pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}'; pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}';
case CryptoCurrency.sol: // case CryptoCurrency.sol: // TODO - looks like this is not correct
pattern = '[1-9A-HJ-NP-Za-km-z]+'; // pattern = '[1-9A-HJ-NP-Za-km-z]+';
case CryptoCurrency.trx: case CryptoCurrency.trx:
pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
case CryptoCurrency.zano: case CryptoCurrency.zano:

View file

@ -1,11 +1,6 @@
import 'package:basic_utils/basic_utils.dart'; 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/src/widgets/picker.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
class Bip353Record { class Bip353Record {
Bip353Record({ Bip353Record({
@ -81,67 +76,4 @@ class Bip353Record {
} }
} }
static Future<String?> pickBip353AddressChoice(
String bip353Name,
Map<String, String> addressMap,
) async {
if (addressMap.length == 1) {
return addressMap.values.first;
}
final chosenAddress = await _showAddressChoiceDialog(bip353Name, addressMap);
return chosenAddress;
}
static Future<String?> _showAddressChoiceDialog(
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 = displayItems.first['originalValue'];
}
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';
}
} }

View file

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/proxy_wrapper.dart';
class FioAddressProvider { class FioAddressProvider {
@ -23,14 +24,13 @@ class FioAddressProvider {
return isFioRegistered; return isFioRegistered;
} }
final responseJSON = json.decode(response.body) as Map<String, dynamic>; final responseJSON = json.decode(response.body) as Map<String, dynamic>;
isFioRegistered = responseJSON['is_registered'] as int == 1; isFioRegistered = responseJSON['is_registered'] as int == 1;
return isFioRegistered; return isFioRegistered;
} }
static Future<String> getPubAddress(String fioAddress, String token) async { static Future<String?> getPubAddress(String fioAddress, String token) async {
final headers = {'Content-Type': 'application/json'}; final headers = {'Content-Type': 'application/json'};
final body = <String, String>{ final body = <String, String>{
"fio_address": fioAddress, "fio_address": fioAddress,
@ -45,21 +45,28 @@ class FioAddressProvider {
body: json.encode(body), body: json.encode(body),
); );
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode == 400) { if (response.statusCode == 400) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error'] as String; final error = responseJSON['error'] as String;
final message = responseJSON['message'] as String; final message = responseJSON['message'] as String;
throw Exception('${error}\n$message'); printV('${error}\n$message');
return null;
} }
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Unexpected response http status: ${response.statusCode}'); final String message = responseJSON['message'] as String? ?? 'Unknown error';
printV('Error fetching public address for token $token: $message');
return null;
} }
final responseJSON = json.decode(response.body) as Map<String, dynamic>; final String pubAddress = responseJSON['public_address'] as String? ?? '';
final String pubAddress = responseJSON['public_address'] as String;
return pubAddress; if (pubAddress.isNotEmpty) {
return pubAddress;
}
return null;
} }
} }

View file

@ -200,7 +200,8 @@ class AddressResolverService {
LookupEntry( LookupEntry(
source: AddressSource.fio, source: AddressSource.fio,
currencies: AddressSource.fio.supportedCurrencies, currencies: AddressSource.fio.supportedCurrencies,
applies: (q) => settingsStore.lookupsFio && !q.startsWith('@') && q.contains('@') && !q.contains('.'), applies: (q) =>
settingsStore.lookupsFio && !q.startsWith('@') && q.contains('@') && !q.contains('.'),
// FIO handle example: username@domain // FIO handle example: username@domain
run: _lookupFio, run: _lookupFio,
), ),
@ -236,7 +237,7 @@ class AddressResolverService {
LookupEntry( LookupEntry(
source: AddressSource.bip353, source: AddressSource.bip353,
currencies: AddressSource.bip353.supportedCurrencies, currencies: AddressSource.bip353.supportedCurrencies,
applies: (q) => settingsStore.lookupsBip353, applies: (q) => settingsStore.lookupsBip353 && q.contains('@') && q.contains('.'),
run: _lookupsBip353, run: _lookupsBip353,
), ),
LookupEntry( LookupEntry(
@ -267,11 +268,8 @@ class AddressResolverService {
]; ];
} }
static String _cleanInput(String raw) =>
raw.replaceAll(RegExp(r'[\u2028\u2029]'), '\n').replaceAll(RegExp(r'<[^>]+>'), ' ');
static String _cleanInput(String raw) => raw
.replaceAll(RegExp(r'[\u2028\u2029]'), '\n')
.replaceAll(RegExp(r'<[^>]+>'), ' ');
static String? extractAddressByType({ static String? extractAddressByType({
required String raw, required String raw,
@ -279,21 +277,21 @@ class AddressResolverService {
}) { }) {
final pat = AddressValidator.getAddressFromStringPattern(type); final pat = AddressValidator.getAddressFromStringPattern(type);
if (pat == null) { if (pat == null) {
throw StateError('Unknown pattern for $type'); printV('Unknown pattern for $type');
return null;
} }
final text = _cleanInput(raw); final text = _cleanInput(raw);
final regex = RegExp(r'(?:^|[^0-9A-Za-z])(' + pat + r')', final regex =
multiLine: true, caseSensitive: false); RegExp(r'(?:^|[^0-9A-Za-z])(' + pat + r')', multiLine: true, caseSensitive: false);
final m = regex.firstMatch(text); final m = regex.firstMatch(text);
if (m == null) return null; if (m == null) return null;
// 3. Strip BCH / NANO prefixes and punctuation just like before // 3. Strip BCH / NANO prefixes and punctuation just like before
final cleaned = m final cleaned =
.group(1)! m.group(1)!.replaceAllMapped(RegExp(r'[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), (m) {
.replaceAllMapped(RegExp(r'[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), (m) {
final g = m.group(0)!; final g = m.group(0)!;
return (g.startsWith('bitcoincash:') || g.startsWith('nano_') || g.startsWith('ban_')) return (g.startsWith('bitcoincash:') || g.startsWith('nano_') || g.startsWith('ban_'))
? g ? g
@ -349,15 +347,21 @@ class AddressResolverService {
final Map<CryptoCurrency, String> result = {}; final Map<CryptoCurrency, String> result = {};
try {
for (final cur in currencies) { for (final cur in currencies) {
final addressFromBio = extractAddressByType( final addressFromBio = extractAddressByType(
raw: twitterUser.description, type: CryptoCurrency.fromString(cur.title)); raw: twitterUser.description, type: CryptoCurrency.fromString(cur.title));
print('Address from bio: $addressFromBio');
if (addressFromBio != null && addressFromBio.isNotEmpty) { if (addressFromBio != null && addressFromBio.isNotEmpty) {
result[cur] = addressFromBio; result[cur] = addressFromBio;
} }
} }
} catch (e) {
printV('Error extracting address from Twitter bio: $e');
}
try {
final pinnedTweet = twitterUser.pinnedTweet?.text; final pinnedTweet = twitterUser.pinnedTweet?.text;
if (pinnedTweet != null) { if (pinnedTweet != null) {
for (final cur in currencies) { for (final cur in currencies) {
@ -368,6 +372,9 @@ class AddressResolverService {
} }
} }
} }
} catch (e) {
printV('Error extracting address from Twitter pinned tweet: $e');
}
if (result.isNotEmpty) { if (result.isNotEmpty) {
return ParsedAddress( return ParsedAddress(
@ -472,7 +479,7 @@ class AddressResolverService {
for (final cur in currencies) { for (final cur in currencies) {
final address = await FioAddressProvider.getPubAddress(text, cur.title); final address = await FioAddressProvider.getPubAddress(text, cur.title);
if (address.isNotEmpty) { if (address != null && address.isNotEmpty) {
result[cur] = address; result[cur] = address;
} }
} }
@ -556,18 +563,24 @@ class AddressResolverService {
} }
Future<ParsedAddress?> _lookupsBip353( Future<ParsedAddress?> _lookupsBip353(
String text, List<CryptoCurrency> currency, WalletBase _) async { String text, List<CryptoCurrency> currencies, WalletBase _) async {
final Map<CryptoCurrency, String> result = {}; final Map<CryptoCurrency, String> result = {};
for (final cur in currency) { for (final cur in currencies) {
final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, cur.title); final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, cur.title);
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) { if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
final address = bip353AddressMap['address']; if (cur == CryptoCurrency.btc) {
if (address != null && address.isNotEmpty) { bip353AddressMap.forEach((key, value) {
result[cur] = address; final address = bip353AddressMap['sp'] ?? bip353AddressMap['address'];
if (address != null && address.isNotEmpty) {
result[cur] = address;
}
});
} }
} }
} }
if (result.isNotEmpty) { if (result.isNotEmpty) {
return ParsedAddress( return ParsedAddress(
parsedAddressByCurrencyMap: result, parsedAddressByCurrencyMap: result,
@ -575,15 +588,6 @@ class AddressResolverService {
handle: text, handle: text,
); );
} }
//
// if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
// final chosenAddress =
// await Bip353Record.pickBip353AddressChoice(text, bip353AddressMap); //TODO fix context
// if (chosenAddress != null) {
// return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
// }
// }
return null; return null;
} }

View file

@ -5,8 +5,9 @@ import 'package:cw_core/crypto_currency.dart';
const supportedSources = [ const supportedSources = [
AddressSource.twitter, AddressSource.twitter,
AddressSource.unstoppableDomains, AddressSource.unstoppableDomains,
AddressSource.ens,
AddressSource.mastodon, AddressSource.mastodon,
AddressSource.bip353,
AddressSource.fio,
]; ];
enum AddressSource { enum AddressSource {
@ -14,7 +15,15 @@ enum AddressSource {
label: 'X', label: 'X',
iconPath: 'assets/images/x_social.png', iconPath: 'assets/images/x_social.png',
alias: '@username', alias: '@username',
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]), supportedCurrencies: [
CryptoCurrency.xmr,
CryptoCurrency.btc,
CryptoCurrency.ltc,
CryptoCurrency.eth,
CryptoCurrency.bnb,
CryptoCurrency.sol,
CryptoCurrency.maticpoly
]),
unstoppableDomains( unstoppableDomains(
label: 'Unstoppable Domains', label: 'Unstoppable Domains',
iconPath: 'assets/images/ud.png', iconPath: 'assets/images/ud.png',
@ -30,19 +39,33 @@ enum AddressSource {
iconPath: 'assets/images/yat_mini_logo.png', iconPath: 'assets/images/yat_mini_logo.png',
alias: '🎂🎂🎂', alias: '🎂🎂🎂',
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]), supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
fio( fio(label: 'FIO', iconPath: 'assets/images/fio.png', alias: 'user@domain', supportedCurrencies: [
label: 'FIO', CryptoCurrency.xmr,
iconPath: 'assets/images/fio.png', CryptoCurrency.btc,
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]), CryptoCurrency.ltc,
CryptoCurrency.eth,
CryptoCurrency.bnb,
CryptoCurrency.sol,
CryptoCurrency.maticpoly,
]),
ens( ens(
label: 'Ethereum Name Service', label: 'Ethereum Name Service',
iconPath: 'assets/images/ens_icon.png', iconPath: 'assets/images/ens_icon.png',
alias: 'domain.eth',
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth]), supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth]),
mastodon( mastodon(
label: 'Mastodon', label: 'Mastodon',
iconPath: 'assets/images/mastodon.svg', iconPath: 'assets/images/mastodon.svg',
alias: 'user@domain.tld', alias: 'user@domain.tld',
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]), supportedCurrencies: [
CryptoCurrency.xmr,
CryptoCurrency.btc,
CryptoCurrency.ltc,
CryptoCurrency.eth,
CryptoCurrency.bnb,
CryptoCurrency.sol,
CryptoCurrency.maticpoly
]),
nostr( nostr(
label: 'Nostr', label: 'Nostr',
iconPath: 'assets/images/nostr.png', iconPath: 'assets/images/nostr.png',
@ -61,8 +84,9 @@ enum AddressSource {
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]), supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]),
bip353( bip353(
label: 'BIP-353', label: 'BIP-353',
iconPath: '', iconPath: 'assets/images/btc.png',
supportedCurrencies: [CryptoCurrency.xmr, CryptoCurrency.btc]), alias: 'user@domain.com',
supportedCurrencies: [CryptoCurrency.btc]),
contact(label: 'Contact', iconPath: '', supportedCurrencies: []), contact(label: 'Contact', iconPath: '', supportedCurrencies: []),
notParsed(label: 'Unknown', iconPath: '', supportedCurrencies: []); notParsed(label: 'Unknown', iconPath: '', supportedCurrencies: []);
@ -93,7 +117,7 @@ extension AddressSourceNameParser on AddressSource {
} }
final needle = text.trim().toLowerCase(); final needle = text.trim().toLowerCase();
return AddressSource.values.firstWhere( return AddressSource.values.firstWhere(
(src) => src.label.toLowerCase() == needle, (src) => src.label.toLowerCase() == needle,
orElse: () => AddressSource.notParsed, orElse: () => AddressSource.notParsed,
); );
} }