mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-24 06:48:38 +02:00
Cw 428 send to nostr addresses (#1271)
* add nostr address resolver * Add Nostr localization --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
@@ -6,12 +6,14 @@ import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/entities/unstoppable_domain_address.dart';
|
||||
import 'package:cake_wallet/entities/emoji_string_extension.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/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/entities/fio_address_provider.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class AddressResolver {
|
||||
AddressResolver({required this.yatService, required this.wallet, required this.settingsStore})
|
||||
@@ -58,7 +60,16 @@ class AddressResolver {
|
||||
});
|
||||
}
|
||||
|
||||
Future<ParsedAddress> resolve(String text, String ticker) async {
|
||||
bool isEmailFormat(String address) {
|
||||
final RegExp emailRegex = RegExp(
|
||||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
return emailRegex.hasMatch(address);
|
||||
}
|
||||
|
||||
|
||||
Future<ParsedAddress> resolve(BuildContext context, String text, String ticker) async {
|
||||
try {
|
||||
if (text.startsWith('@') && !text.substring(1).contains('@')) {
|
||||
if(settingsStore.lookupsTwitter) {
|
||||
@@ -165,6 +176,21 @@ class AddressResolver {
|
||||
}
|
||||
}
|
||||
}
|
||||
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: CryptoCurrency.fromString(ticker));
|
||||
if (addressFromBio != null) {
|
||||
return ParsedAddress.nostrAddress(address: addressFromBio, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
|
||||
@@ -2,7 +2,18 @@ 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 }
|
||||
enum ParseFrom {
|
||||
unstoppableDomains,
|
||||
openAlias,
|
||||
yatRecord,
|
||||
fio,
|
||||
notParsed,
|
||||
twitter,
|
||||
ens,
|
||||
contact,
|
||||
mastodon,
|
||||
nostr
|
||||
}
|
||||
|
||||
class ParsedAddress {
|
||||
ParsedAddress({
|
||||
@@ -11,9 +22,9 @@ class ParsedAddress {
|
||||
this.description = '',
|
||||
this.parseFrom = ParseFrom.notParsed,
|
||||
});
|
||||
|
||||
|
||||
factory ParsedAddress.fetchEmojiAddress({
|
||||
List<YatRecord>? addresses,
|
||||
List<YatRecord>? addresses,
|
||||
required String name,
|
||||
}){
|
||||
if (addresses?.isEmpty ?? true) {
|
||||
@@ -28,7 +39,7 @@ class ParsedAddress {
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchUnstoppableDomainAddress({
|
||||
String? address,
|
||||
String? address,
|
||||
required String name,
|
||||
}){
|
||||
if (address?.isEmpty ?? true) {
|
||||
@@ -94,6 +105,14 @@ class ParsedAddress {
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.nostrAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.nostr,
|
||||
);
|
||||
}
|
||||
|
||||
final List<String> addresses;
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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: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) {
|
||||
print('[!] 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) {
|
||||
print('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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
class UserMetadata {
|
||||
final String name;
|
||||
final String lnurl;
|
||||
final String email;
|
||||
final String picture;
|
||||
final String about;
|
||||
final String nip05;
|
||||
final String banner;
|
||||
final String website;
|
||||
|
||||
UserMetadata({
|
||||
required this.name,
|
||||
required this.lnurl,
|
||||
required this.email,
|
||||
required this.picture,
|
||||
required this.about,
|
||||
required this.nip05,
|
||||
required this.banner,
|
||||
required this.website,
|
||||
});
|
||||
|
||||
factory UserMetadata.fromJson(Map<String, dynamic> json) {
|
||||
return UserMetadata(
|
||||
name: json['name'] as String? ?? '',
|
||||
lnurl: json['lud06'] as String? ?? '',
|
||||
email: json['lud16'] as String? ?? '',
|
||||
picture: json['picture'] as String? ?? '',
|
||||
about: json['about'] as String? ?? '',
|
||||
nip05: json['nip05'] as String? ?? '',
|
||||
banner: json['banner'] as String? ?? '',
|
||||
website: json['website'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -557,7 +557,7 @@ class ExchangePage extends BasePage {
|
||||
}
|
||||
|
||||
Future<String> fetchParsedAddress(BuildContext context, String domain, String ticker) async {
|
||||
final parsedAddress = await getIt.get<AddressResolver>().resolve(domain, ticker);
|
||||
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, ticker);
|
||||
final address = await extractAddressFromParsed(context, parsedAddress);
|
||||
return address;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ Future<String> extractAddressFromParsed(
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)');
|
||||
address = parsedAddress.addresses.first;
|
||||
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;
|
||||
break;
|
||||
case ParseFrom.yatRecord:
|
||||
if (parsedAddress.name.isEmpty) {
|
||||
title = S.of(context).yat_error;
|
||||
|
||||
@@ -262,7 +262,7 @@ abstract class OutputBase with Store {
|
||||
Future<void> fetchParsedAddress(BuildContext context) async {
|
||||
final domain = address;
|
||||
final ticker = cryptoCurrencyHandler().title.toLowerCase();
|
||||
parsedAddress = await getIt.get<AddressResolver>().resolve(domain, ticker);
|
||||
parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, ticker);
|
||||
extractedAddress = await extractAddressFromParsed(context, parsedAddress);
|
||||
note = parsedAddress.description;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user