From 71706314fb5594859c2f12d1fbb3bfed7e03e0d8 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 15 Aug 2026 16:28:54 -0400 Subject: [PATCH] privacy: only domain-anchored address resolvers; drop Cake endpoints Address-field input no longer leaves the device except toward infrastructure implied by the typed name itself: - remove Twitter, Mastodon, Nostr, Yat, Unstoppable Domains, FIO, ThorChain-name, zcash.me, and Zano alias lookups (Nostr and FIO ran with no setting gate at all; Zano is not even a supported chain) - keep OpenAlias, BIP-353, .well-known, LNURL, and ENS, each toggleable in Settings -> Domain lookups - BTC fee/block/restore-height queries move from mempool.cakewallet.com to public mempool.space (also fixes two /api/v1 paths that 404 on standard mempool deployments) - PRIVACY.md updated to disclose the remaining flows (mempool.space, name lookups, opt-in silent payments and payjoin); issue templates and the translation prompt de-Caked --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- .github/ISSUE_TEMPLATE/config.yml | 11 +- PRIVACY.md | 23 +- cw_bitcoin/lib/electrum_wallet.dart | 12 +- cw_core/lib/get_height_by_date.dart | 2 +- lib/core/yat_service.dart | 55 --- lib/di.dart | 4 - lib/entities/emoji_string_extension.dart | 26 -- lib/entities/fio_address_provider.dart | 65 --- lib/entities/parse_address_from_domain.dart | 411 +----------------- lib/entities/unstoppable_domain_address.dart | 30 -- lib/entities/zano_alias.dart | 29 -- lib/mastodon/mastodon_api.dart | 66 --- lib/mastodon/mastodon_user.dart | 39 -- lib/nostr/nostr_api.dart | 150 ------- lib/nostr/nostr_user.dart | 35 -- .../screens/settings/domain_lookups_page.dart | 22 - lib/twitter/twitter_api.dart | 67 --- lib/twitter/twitter_user.dart | 49 --- tool/translate_using_llm.dart | 6 +- 20 files changed, 48 insertions(+), 1056 deletions(-) delete mode 100644 lib/core/yat_service.dart delete mode 100644 lib/entities/emoji_string_extension.dart delete mode 100644 lib/entities/fio_address_provider.dart delete mode 100644 lib/entities/unstoppable_domain_address.dart delete mode 100644 lib/entities/zano_alias.dart delete mode 100644 lib/mastodon/mastodon_api.dart delete mode 100644 lib/mastodon/mastodon_user.dart delete mode 100644 lib/nostr/nostr_api.dart delete mode 100644 lib/nostr/nostr_user.dart delete mode 100644 lib/twitter/twitter_api.dart delete mode 100644 lib/twitter/twitter_user.dart diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 457cb84a4..38d48f727 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -26,7 +26,7 @@ If applicable, add screenshots to help explain your problem. **Platform:** - OS: [e.g. iOS 15.1, Android 14] - Device: [e.g. iPhone 14, Galaxy S21] - - Cake Wallet Version: [e.g. 4.12.1] + - Hash Bags Version: [e.g. 1.0.0] **Additional context** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f8cc8f9ca..71b04cd75 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,8 @@ blank_issues_enabled: false contact_links: - name: Feature or Enhancement Request ✨ - url: https://github.com/cake-tech/cake_wallet/discussions/new?category=feature-requests - about: Suggest an idea for Cake Wallet - - name: Not sure where to start? - url: https://docs.cakewallet.com - about: Start by reading checking out the guides! + url: https://github.com/Such-Software/hash-wallet/discussions + about: Suggest an idea for Hash Bags - name: Need help? - url: https://cakewallet.com/#contact - about: Use our live chat or send a support email! + url: https://hash.boats + about: Guides, support, and contact info diff --git a/PRIVACY.md b/PRIVACY.md index c83b9e730..b3bc666ba 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,6 +1,6 @@ # Privacy Policy -**Effective: July 2, 2026** +**Effective: August 15, 2026** The canonical, always-current version of this policy is published at **https://hash.boats/privacy**. This file mirrors it for the source repository; @@ -68,6 +68,27 @@ phrase still restores the wallet elsewhere. - **Price data.** The app fetches exchange rates from `prices.neroswap.com`, a service we operate that aggregates public sources. It sees your IP and the currencies you display — not your addresses or balances. +- **Bitcoin fee and block data.** For Bitcoin wallets, the app queries the + public `mempool.space` API for recommended fee rates and block timestamps. + It sees your IP and the block heights queried — not your addresses or + transactions. This is on by default and can be turned off in settings. +- **Name lookups.** If you type a human-readable name instead of an address, + the app can resolve it. Every resolver is limited to infrastructure implied + by the name itself, and each can be toggled in Settings → Domain lookups: + OpenAlias and BIP-353 query DNS; `.well-known` and Lightning-address + lookups contact the domain in the identifier you typed; ENS (`.eth` names) + queries the public Ethereum RPC at `ethereum-rpc.publicnode.com`. Hash Bags + does not send what you type to social networks or third-party identity + services. +- **Silent Payments scanning (opt-in, Bitcoin).** If you enable Silent + Payments, scanning uses a compatible Electrum server (the current default, + `electrs.cakewallet.com`, is operated by Cake Labs, a third party). That + server sees your IP and the block ranges you scan — not your keys. You can + point it at your own server in settings. +- **Payjoin (opt-in, Bitcoin, off by default).** If you enable Payjoin, + traffic is relayed through an oblivious-HTTP relay chosen from a small + public pool (currently operated by bobspacebkk.com, achow101.com, and + Cake Labs). The relay sees your IP but not the transaction contents. - **Trocador (swaps).** When you start a swap, order details (input/output coin, amount, receiving address) are sent to Trocador's API. Trocador is an independent controller with its own policy at diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 243a88b26..9b2cc2631 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -748,7 +748,7 @@ abstract class ElectrumWalletBase if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) { try { final response = await ProxyWrapper() - .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) + .get(clearnetUri: Uri.parse("https://mempool.space/api/v1/fees/recommended")) .timeout(Duration(seconds: 15)); final result = json.decode(response.body) as Map; @@ -2292,7 +2292,7 @@ abstract class ElectrumWalletBase final blockHash = await ProxyWrapper() .get( clearnetUri: Uri.parse( - "https://mempool.cakewallet.com/api/v1/block-height/$height", + "https://mempool.space/api/block-height/$height", ), ) .timeout(Duration(seconds: 15)); @@ -2303,7 +2303,7 @@ abstract class ElectrumWalletBase final blockResponse = await ProxyWrapper() .get( clearnetUri: Uri.parse( - "https://mempool.cakewallet.com/api/v1/block/${blockHash.body}", + "https://mempool.space/api/block/${blockHash.body}", ), ) .timeout(Duration(seconds: 15)); @@ -3137,7 +3137,7 @@ abstract class ElectrumWalletBase final blockHashResp = await ProxyWrapper() .get( clearnetUri: Uri.parse( - 'https://mempool.cakewallet.com/api/v1/block-height/$h', + 'https://mempool.space/api/block-height/$h', ), ) .timeout(const Duration(seconds: 15)); @@ -3150,7 +3150,7 @@ abstract class ElectrumWalletBase final blockResp = await ProxyWrapper() .get( clearnetUri: Uri.parse( - 'https://mempool.cakewallet.com/api/v1/block/$blockHash', + 'https://mempool.space/api/block/$blockHash', ), ) .timeout(const Duration(seconds: 15)); @@ -4086,7 +4086,7 @@ Future _handleScanSilentPayments(ScanData scanData) async { // So, if blockDate exists, reuse if (isDateNow) { try { - final rootURL = "https://cake.mempool.space"; + final rootURL = "https://mempool.space"; final tweakBlockHash = await ProxyWrapper() .get(clearnetUri: Uri.parse("$rootURL/api/block-height/$tweakHeight")) .timeout(Duration(seconds: 15)); diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 58155a7fb..a220f4381 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -127,7 +127,7 @@ Future getBitcoinHeightByDateAPI({required DateTime date}) async { final req = await ProxyWrapper() .getHttpClient() .getUrl(Uri.parse( - "https://mempool.cakewallet.com/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}")) + "https://mempool.space/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}")) .timeout(Duration(seconds: 15)); final response = await req.close(); final stringResponse = await response.transform(utf8.decoder).join(); diff --git a/lib/core/yat_service.dart b/lib/core/yat_service.dart deleted file mode 100644 index 04bfe7bb2..000000000 --- a/lib/core/yat_service.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_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> 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: { - "tags": tag - }); - final yatRecords = []; - - try { - final response = await ProxyWrapper().get(clearnetUri: uri); - - final resBody = json.decode(response.body) as Map; - final results = resBody["result"] as Map; - // Favour a subaddress over a standard address. - final yatRecord = ( - results[MONERO_SUB_ADDRESS] ?? - results[MONERO_STD_ADDRESS] ?? - results[tag]) as Map; - - if (yatRecord.isNotEmpty) { - yatRecords.add(YatRecord.fromJson(yatRecord)); - } - - return yatRecords; - } catch (_) { - return yatRecords; - } - } -} diff --git a/lib/di.dart b/lib/di.dart index a0503b12b..29f32218c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -29,7 +29,6 @@ import 'package:hash_wallet/core/totp_request_details.dart'; import 'package:hash_wallet/core/trade_monitor.dart'; import 'package:hash_wallet/core/wallet_creation_service.dart'; import 'package:hash_wallet/core/wallet_loading_service.dart'; -import 'package:hash_wallet/core/yat_service.dart'; import 'package:hash_wallet/entities/biometric_auth.dart'; import 'package:hash_wallet/entities/bridge_transfer.dart'; import 'package:hash_wallet/entities/contact.dart'; @@ -1580,10 +1579,7 @@ Future setup({ param1: item, param2: unspentCoinsListViewModel)); }); - getIt.registerFactory(() => YatService()); - getIt.registerFactory(() => AddressResolver( - yatService: getIt.get(), wallet: getIt.get().wallet!, settingsStore: getIt.get())); diff --git a/lib/entities/emoji_string_extension.dart b/lib/entities/emoji_string_extension.dart deleted file mode 100644 index 8853c3743..000000000 --- a/lib/entities/emoji_string_extension.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; - -extension Emoji on String { - static final REGEX_EMOJI = RegExp( - r'[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}' - r'\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}' - r'-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}' - r'\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}' - r'-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}' - r'\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}' - r'-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}' - r'\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}' - r'-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}\u{200d}]+', - unicode: true, - ); - - bool _hasOnlyEmojis() { - final parsedText = this.replaceAll(' ', ''); - if(parsedText.isEmpty) return false; - for (final c in Characters(parsedText)) - if (!REGEX_EMOJI.hasMatch(c)) return false; - return true; - } - /// Returns true if the given text contains only emojis. - bool get hasOnlyEmojis => _hasOnlyEmojis(); -} diff --git a/lib/entities/fio_address_provider.dart b/lib/entities/fio_address_provider.dart deleted file mode 100644 index dcfd79c96..000000000 --- a/lib/entities/fio_address_provider.dart +++ /dev/null @@ -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 checkAvail(String fioAddress) async { - bool isFioRegistered = false; - final headers = {'Content-Type': 'application/json'}; - final body = {"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; - isFioRegistered = responseJSON['is_registered'] as int == 1; - - return isFioRegistered; - } - - static Future getPubAddress(String fioAddress, String token) async { - final headers = {'Content-Type': 'application/json'}; - final body = { - "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; - 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; - final String pubAddress = responseJSON['public_address'] as String; - - return pubAddress; - } -} diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 07204b9bf..5e9126826 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -1,175 +1,35 @@ import 'package:hash_wallet/core/address_validator.dart'; -import 'package:hash_wallet/core/yat_service.dart'; -import 'package:hash_wallet/entities/emoji_string_extension.dart'; import 'package:hash_wallet/entities/ens_record.dart'; import 'package:hash_wallet/entities/lnurlpay_record.dart'; -import 'package:hash_wallet/entities/fio_address_provider.dart'; import 'package:hash_wallet/entities/openalias_record.dart'; import 'package:hash_wallet/entities/parsed_address.dart'; -import 'package:hash_wallet/entities/unstoppable_domain_address.dart'; import 'package:hash_wallet/entities/wellknown_record.dart'; -import 'package:hash_wallet/entities/zano_alias.dart'; -import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart'; -import 'package:hash_wallet/mastodon/mastodon_api.dart'; -import 'package:hash_wallet/nostr/nostr_api.dart'; import 'package:hash_wallet/store/settings_store.dart'; -import 'package:hash_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'; +/// Resolves human-readable names typed into the address field. +/// +/// Privacy contract: every lookup here either queries DNS (OpenAlias, +/// BIP-353), the domain the user explicitly typed (.well-known, LNURL-pay), +/// or the wallet's own configured RPC node (ENS). Resolvers that shipped the +/// typed input to unrelated third-party services (Twitter, Mastodon, Nostr, +/// Yat, Unstoppable Domains, FIO, ThorChain names, zcash.me, Zano aliases) +/// were removed deliberately — do not reintroduce one without updating +/// PRIVACY.md and gating it behind an off-by-default setting. class AddressResolver { - AddressResolver({required this.yatService, required this.wallet, required this.settingsStore}) + AddressResolver({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, @@ -196,17 +56,6 @@ class AddressResolver { }); } - static String extractUnstoppableDomain(String raw) { - // sort by length to avoid matching shorter tld instead of longer - final domains = List.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,}$', @@ -218,167 +67,7 @@ class AddressResolver { Future 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) { - String? addressFromBio = extractAddressByType(raw: mastodonUser.note, 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) => 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: + // user@domain identifiers: resolved against the domain the user typed. if (text.contains('.') && text.contains('@')) { if (settingsStore.lookupsWellKnown) { final record = @@ -397,33 +86,6 @@ class AddressResolver { } } - 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) { - 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; @@ -432,15 +94,6 @@ class AddressResolver { 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) { @@ -476,52 +129,10 @@ 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: 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 _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; - } } diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart deleted file mode 100644 index cf6eb8945..000000000 --- a/lib/entities/unstoppable_domain_address.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:convert'; - -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; - -Future 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; - if (jsonParsed["records"] == null) { - throw Exception(".records response from $uri is empty"); - }; - final records = jsonParsed["records"] as Map; - 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; -} \ No newline at end of file diff --git a/lib/entities/zano_alias.dart b/lib/entities/zano_alias.dart deleted file mode 100644 index 3d1c74969..000000000 --- a/lib/entities/zano_alias.dart +++ /dev/null @@ -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 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; - - return jsonParsed['result']['alias_details']['address'] as String?; - } catch (e) { - printV('Zano Alias error: ${e.toString()}'); - } - - return null; - } -} diff --git a/lib/mastodon/mastodon_api.dart b/lib/mastodon/mastodon_api.dart deleted file mode 100644 index 7916c11c1..000000000 --- a/lib/mastodon/mastodon_api.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:convert'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hash_wallet/mastodon/mastodon_user.dart'; - -class MastodonAPI { - static const httpsScheme = 'https'; - static const userPath = '/api/v1/accounts/lookup'; - static const statusesPath = '/api/v1/accounts/:id/statuses'; - - static Future lookupUserByUserName( - {required String userName, required String apiHost}) async { - try { - final queryParams = {'acct': userName}; - - final uri = Uri( - scheme: httpsScheme, - host: apiHost, - path: userPath, - queryParameters: queryParams, - ); - - final response = await ProxyWrapper().get(clearnetUri: uri); - - - if (response.statusCode != 200) return null; - - final Map responseJSON = json.decode(response.body) as Map; - - return MastodonUser.fromJson(responseJSON); - } catch (e) { - printV('Error in lookupUserByUserName: $e'); - return null; - } - } - - static Future> getPinnedPosts({ - required String userId, - required String apiHost, - }) async { - try { - final queryParams = {'pinned': 'true'}; - - final uri = Uri( - scheme: httpsScheme, - host: apiHost, - path: statusesPath.replaceAll(':id', userId), - queryParameters: queryParams, - ); - - final response = await ProxyWrapper().get(clearnetUri: uri); - - - if (response.statusCode != 200) { - throw Exception('Unexpected HTTP status: ${response.statusCode}'); - } - - final List responseJSON = json.decode(response.body) as List; - - return responseJSON.map((json) => PinnedPost.fromJson(json as Map)).toList(); - } catch (e) { - printV('Error in getPinnedPosts: $e'); - throw e; - } - } -} diff --git a/lib/mastodon/mastodon_user.dart b/lib/mastodon/mastodon_user.dart deleted file mode 100644 index 1832c083e..000000000 --- a/lib/mastodon/mastodon_user.dart +++ /dev/null @@ -1,39 +0,0 @@ -class MastodonUser { - String id; - String username; - String profileImageUrl; - String acct; - String note; - - MastodonUser({ - required this.id, - required this.username, - required this.profileImageUrl, - required this.acct, - required this.note, - }); - - factory MastodonUser.fromJson(Map json) { - return MastodonUser( - id: json['id'] as String, - username: json['username'] as String? ?? '', - acct: json['acct'] as String, - note: json['note'] as String, - profileImageUrl: json['avatar'] as String? ?? '' - ); - } -} - -class PinnedPost { - final String id; - final String content; - - PinnedPost({required this.id, required this.content}); - - factory PinnedPost.fromJson(Map json) { - return PinnedPost( - id: json['id'] as String, - content: json['content'] as String, - ); - } -} diff --git a/lib/nostr/nostr_api.dart b/lib/nostr/nostr_api.dart deleted file mode 100644 index 8826910eb..000000000 --- a/lib/nostr/nostr_api.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/nostr/nostr_user.dart'; -import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:hash_wallet/src/widgets/picker.dart'; -import 'package:hash_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 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 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 _fetchInfoFromRelay( - String relayUrl, String userPubKey, List 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; - - 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 _showErrorDialog( - BuildContext context, String title, String errorMessage) async { - if (context.mounted) { - await showPopUp( - 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 _showRelayChoiceDialog( - BuildContext context, - List relays, - ) async { - String? selectedRelay; - - if (context.mounted) { - await showPopUp( - context: context, - builder: (BuildContext dialogContext) { - return Picker( - selectedAtIndex: 0, - title: S.of(dialogContext).choose_relay, - items: relays, - onItemSelected: (String relay) => selectedRelay = relay, - ); - }, - ); - } - - return selectedRelay; - } -} diff --git a/lib/nostr/nostr_user.dart b/lib/nostr/nostr_user.dart deleted file mode 100644 index 4b1694a11..000000000 --- a/lib/nostr/nostr_user.dart +++ /dev/null @@ -1,35 +0,0 @@ - -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 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? ?? '', - ); - } -} \ No newline at end of file diff --git a/lib/src/screens/settings/domain_lookups_page.dart b/lib/src/screens/settings/domain_lookups_page.dart index 673cd12d3..1955b884e 100644 --- a/lib/src/screens/settings/domain_lookups_page.dart +++ b/lib/src/screens/settings/domain_lookups_page.dart @@ -21,22 +21,6 @@ class DomainLookupsPage extends BasePage { 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, @@ -49,12 +33,6 @@ class DomainLookupsPage extends BasePage { 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 ], ), ); diff --git a/lib/twitter/twitter_api.dart b/lib/twitter/twitter_api.dart deleted file mode 100644 index a198cdc68..000000000 --- a/lib/twitter/twitter_api.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/twitter/twitter_user.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; - -class TwitterApi { - static const twitterBearerToken = secrets.twitterBearerToken; - static const httpsScheme = 'https'; - static const apiHost = 'api.twitter.com'; - static const userPath = '/2/users/by/username/'; - - static Future lookupUserByName({required String userName}) async { - final queryParams = { - 'user.fields': 'description,profile_image_url,location', - 'expansions': 'pinned_tweet_id', - 'tweet.fields': 'note_tweet' - }; - final headers = {'authorization': 'Bearer $twitterBearerToken'}; - final uri = Uri( - scheme: httpsScheme, - host: apiHost, - path: userPath + userName, - queryParameters: queryParams); - - final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers).catchError((error) { - throw Exception('HTTP request failed: $error'); - }); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - - final Map responseJSON = jsonDecode(response.body) as Map; - if (responseJSON['errors'] != null && - !responseJSON['errors'][0]['detail'] - .toString() - .contains("Could not find tweet with pinned_tweet_id")) { - throw Exception(responseJSON['errors'][0]['detail']); - } - - return TwitterUser.fromJson(responseJSON, _getPinnedTweet(responseJSON)); - } - - static Tweet? _getPinnedTweet(Map responseJSON) { - try { - final tweetId = responseJSON['data']['pinned_tweet_id'] as String?; - if (tweetId == null || responseJSON['includes'] == null) return null; - - final tweetIncludes = List.from(responseJSON['includes']['tweets'] as List); - final pinnedTweetData = tweetIncludes.firstWhere( - (tweet) => tweet['id'] == tweetId, - orElse: () => null, - ) as Map?; - - if (pinnedTweetData == null) return null; - - final pinnedTweetText = - (pinnedTweetData['note_tweet']?['text'] ?? pinnedTweetData['text']) as String; - - return Tweet(id: tweetId, text: pinnedTweetText); - } catch (e) { - return null; - } - } -} diff --git a/lib/twitter/twitter_user.dart b/lib/twitter/twitter_user.dart deleted file mode 100644 index f124de405..000000000 --- a/lib/twitter/twitter_user.dart +++ /dev/null @@ -1,49 +0,0 @@ -class TwitterUser { - TwitterUser( - {required this.id, - required this.username, - required this.name, - required this.description, - required this.location, - required this.profileImageUrl, - this.pinnedTweet}); - - final String id; - final String username; - final String name; - final String description; - final String location; - final String profileImageUrl; - final Tweet? pinnedTweet; - - factory TwitterUser.fromJson(Map json, [Tweet? pinnedTweet]) { - final profileImageUrl = json['data']['profile_image_url'] as String? ?? ''; - final scaledProfileImageUrl = profileImageUrl.replaceFirst('normal', '200x200'); - return TwitterUser( - id: json['data']['id'] as String, - username: json['data']['username'] as String? ?? '', - name: json['data']['name'] as String, - description: json['data']['description'] as String? ?? '', - location: json['data']['location'] as String? ?? '', - profileImageUrl: scaledProfileImageUrl, - pinnedTweet: pinnedTweet, - ); - } -} - -class Tweet { - Tweet({ - required this.id, - required this.text, - }); - - final String id; - final String text; - - factory Tweet.fromJson(Map json) { - return Tweet( - id: json['id'] as String, - text: json['text'] as String, - ); - } -} diff --git a/tool/translate_using_llm.dart b/tool/translate_using_llm.dart index 3ca92d784..f6599bd11 100644 --- a/tool/translate_using_llm.dart +++ b/tool/translate_using_llm.dart @@ -121,13 +121,13 @@ class TranslationChecker { required String destinationFile, }) { return ''' -You are a professional translator checking the accuracy of a translation for a cryptocurrency wallet called Cake Wallet. +You are a professional translator checking the accuracy of a translation for a cryptocurrency wallet called Hash Bags. source file is: ${sourceFile}, destination file is: ${destinationFile}. Rules you must obey: -- respect company branding and style (Cake Wallet, Cake Pay, Bird Pay and other brandings stay the same no matter the language) -- be accurate and remember that Cake Wallet lets you hold cryptocurrency, so use words that are associated with cryptocurrency and wallets more preferably than words that are associated with banks. +- respect company branding and style (the product name "Hash Bags" stays the same no matter the language; never introduce "Cake Wallet" or other third-party brand names) +- be accurate and remember that Hash Bags lets you hold cryptocurrency, so use words that are associated with cryptocurrency and wallets more preferably than words that are associated with banks. - elements with \${value} should remain non-translated as they are used by codegen to dynamically insert the value. - make sure that the translation that you output improves the original translation, and doesn't change the meaning of it. - if original translation contains abbreviations, make sure to translate them as well, and do not expand them.