From 60e7dcffa957278866829b3d9f3279d452da3e67 Mon Sep 17 00:00:00 2001 From: Matthew Fosse <matt@fosse.co> Date: Wed, 15 Jan 2025 17:03:30 -0500 Subject: [PATCH] .well-known domain support (#1956) * add well-known setting [wip] * should work * fix * minor fix (tested and working) --- lib/core/backup_service.dart | 6 ++ lib/entities/parse_address_from_domain.dart | 12 +++ lib/entities/parsed_address.dart | 11 ++- lib/entities/preferences_key.dart | 1 + lib/entities/wellknown_record.dart | 92 +++++++++++++++++++ .../widgets/extract_address_from_parsed.dart | 5 + .../screens/settings/domain_lookups_page.dart | 4 + lib/store/settings_store.dart | 11 +++ .../settings/privacy_settings_view_model.dart | 6 ++ 9 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 lib/entities/wellknown_record.dart diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index f101ed7e1..03f20363d 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -293,6 +293,7 @@ class BackupService { final lookupsUnstoppableDomains = data[PreferencesKey.lookupsUnstoppableDomains] as bool?; final lookupsOpenAlias = data[PreferencesKey.lookupsOpenAlias] as bool?; final lookupsENS = data[PreferencesKey.lookupsENS] as bool?; + final lookupsWellKnown = data[PreferencesKey.lookupsWellKnown] as bool?; final syncAll = data[PreferencesKey.syncAllKey] as bool?; final syncMode = data[PreferencesKey.syncModeKey] as int?; final autoGenerateSubaddressStatus = @@ -403,6 +404,9 @@ class BackupService { if (lookupsENS != null) await _sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS); + if (lookupsWellKnown != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, lookupsWellKnown); + if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); if (syncMode != null) await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); @@ -542,6 +546,8 @@ class BackupService { _sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains), PreferencesKey.lookupsOpenAlias: _sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias), PreferencesKey.lookupsENS: _sharedPreferences.getBool(PreferencesKey.lookupsENS), + PreferencesKey.lookupsWellKnown: + _sharedPreferences.getBool(PreferencesKey.lookupsWellKnown), PreferencesKey.syncModeKey: _sharedPreferences.getInt(PreferencesKey.syncModeKey), PreferencesKey.syncAllKey: _sharedPreferences.getBool(PreferencesKey.syncAllKey), PreferencesKey.autoGenerateSubaddressStatusKey: diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index b13dfa9ad..9be125081 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -5,6 +5,7 @@ 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/emoji_string_extension.dart'; +import 'package:cake_wallet/entities/wellknown_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'; @@ -208,6 +209,17 @@ class AddressResolver { } } + // .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 (!text.startsWith('@') && text.contains('@') && !text.contains('.')) { final bool isFioRegistered = await FioAddressProvider.checkAvail(text); if (isFioRegistered) { diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index cfd69acbe..eabc606db 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -12,7 +12,8 @@ enum ParseFrom { contact, mastodon, nostr, - thorChain + thorChain, + wellKnown } class ParsedAddress { @@ -142,6 +143,14 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchWellKnownAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.wellKnown, + ); + } + final List<String> addresses; final String name; final String description; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 58a540278..4955690e2 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -76,6 +76,7 @@ class PreferencesKey { static const lookupsUnstoppableDomains = 'looks_up_unstoppable_domain'; static const lookupsOpenAlias = 'looks_up_open_alias'; static const lookupsENS = 'looks_up_ens'; + static const lookupsWellKnown = 'looks_up_well_known'; static const showCameraConsent = 'show_camera_consent'; static String moneroWalletUpdateV1Key(String name) => diff --git a/lib/entities/wellknown_record.dart b/lib/entities/wellknown_record.dart new file mode 100644 index 000000000..edc972f76 --- /dev/null +++ b/lib/entities/wellknown_record.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:http/http.dart' as http; + +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 http.Response response = await http.get( + 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; + + print("formattedName: $formattedName"); + + final address = await checkWellKnownUsername(formattedName, currency); + + if (address == null) { + return null; + } + + return WellKnownRecord(address: address, name: name); + } +} diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index 9ce3ca2b1..106be97ee 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -30,6 +30,11 @@ Future<String> extractAddressFromParsed( 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)'); diff --git a/lib/src/screens/settings/domain_lookups_page.dart b/lib/src/screens/settings/domain_lookups_page.dart index aa7e68cd0..0eb559817 100644 --- a/lib/src/screens/settings/domain_lookups_page.dart +++ b/lib/src/screens/settings/domain_lookups_page.dart @@ -45,6 +45,10 @@ class DomainLookupsPage extends BasePage { title: 'Ethereum Name Service', value: _privacySettingsViewModel.looksUpENS, onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsENS(value)), + SettingsSwitcherCell( + title: '.well-known', + value: _privacySettingsViewModel.looksUpWellKnown, + onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsWellKnown(value)), //if (!isHaven) it does not work correctly ], diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index aa7df4ba9..318be637e 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -115,6 +115,7 @@ abstract class SettingsStoreBase with Store { required this.lookupsUnstoppableDomains, required this.lookupsOpenAlias, required this.lookupsENS, + required this.lookupsWellKnown, required this.customBitcoinFeeRate, required this.silentPaymentsCardDisplay, required this.silentPaymentsAlwaysScan, @@ -459,6 +460,11 @@ abstract class SettingsStoreBase with Store { reaction((_) => lookupsENS, (bool looksUpENS) => _sharedPreferences.setBool(PreferencesKey.lookupsENS, looksUpENS)); + reaction( + (_) => lookupsWellKnown, + (bool looksUpWellKnown) => + _sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown)); + // secure storage keys: reaction( (_) => allowBiometricalAuthentication, @@ -772,6 +778,8 @@ abstract class SettingsStoreBase with Store { @observable bool lookupsENS; + @observable + bool lookupsWellKnown; @observable SyncMode currentSyncMode; @@ -967,6 +975,7 @@ 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 lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true; final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1; final silentPaymentsCardDisplay = sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; @@ -1245,6 +1254,7 @@ abstract class SettingsStoreBase with Store { lookupsUnstoppableDomains: lookupsUnstoppableDomains, lookupsOpenAlias: lookupsOpenAlias, lookupsENS: lookupsENS, + lookupsWellKnown: lookupsWellKnown, customBitcoinFeeRate: customBitcoinFeeRate, silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, @@ -1414,6 +1424,7 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; + lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true; customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1; silentPaymentsCardDisplay = sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index eaa9f9e84..67f0d88a0 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -94,6 +94,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get looksUpENS => _settingsStore.lookupsENS; + @computed + bool get looksUpWellKnown => _settingsStore.lookupsWellKnown; + bool get canUseEtherscan => _wallet.type == WalletType.ethereum; bool get canUsePolygonScan => _wallet.type == WalletType.polygon; @@ -130,6 +133,9 @@ abstract class PrivacySettingsViewModelBase with Store { @action void setLookupsENS(bool value) => _settingsStore.lookupsENS = value; + @action + void setLookupsWellKnown(bool value) => _settingsStore.lookupsWellKnown = value; + @action void setLookupsYatService(bool value) => _settingsStore.lookupsYatService = value;