mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
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
This commit is contained in:
@@ -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**
|
||||
|
||||
@@ -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
|
||||
|
||||
+22
-1
@@ -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
|
||||
|
||||
@@ -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<String, dynamic>;
|
||||
@@ -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<void> _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));
|
||||
|
||||
@@ -127,7 +127,7 @@ Future<int> 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();
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> setup({
|
||||
param1: item, param2: unspentCoinsListViewModel));
|
||||
});
|
||||
|
||||
getIt.registerFactory(() => YatService());
|
||||
|
||||
getIt.registerFactory(() => AddressResolver(
|
||||
yatService: getIt.get<YatService>(),
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
settingsStore: getIt.get<SettingsStore>()));
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<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,}$',
|
||||
@@ -218,167 +67,7 @@ class AddressResolver {
|
||||
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) {
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<MastodonUser?> 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<String, dynamic> responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
return MastodonUser.fromJson(responseJSON);
|
||||
} catch (e) {
|
||||
printV('Error in lookupUserByUserName: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<PinnedPost>> 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<dynamic> responseJSON = json.decode(response.body) as List<dynamic>;
|
||||
|
||||
return responseJSON.map((json) => PinnedPost.fromJson(json as Map<String, dynamic>)).toList();
|
||||
} catch (e) {
|
||||
printV('Error in getPinnedPosts: $e');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> 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<String, dynamic> json) {
|
||||
return PinnedPost(
|
||||
id: json['id'] as String,
|
||||
content: json['content'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
@@ -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<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? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<TwitterUser> 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<String, dynamic> responseJSON = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
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<String, dynamic> 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<String, dynamic>?;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> 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<String, dynamic> json) {
|
||||
return Tweet(
|
||||
id: json['id'] as String,
|
||||
text: json['text'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user