mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-11-01 07:04:08 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
|
||||
import 'package:hash_wallet/core/address_resolver/address_resolver_utils.dart';
|
||||
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
|
||||
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:hash_wallet/core/address_resolver/twitter/twitter_api.dart';
|
||||
import 'package:hash_wallet/core/address_resolver/twitter/twitter_user.dart';
|
||||
import 'package:hash_wallet/core/address_validator.dart';
|
||||
import 'package:hash_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class TwitterAddressProvider extends AddressLookupProvider {
|
||||
@override
|
||||
AddressSource get source => AddressSource.twitter;
|
||||
|
||||
@override
|
||||
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsTwitter;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) =>
|
||||
query.startsWith('@') && !query.substring(1).contains('@'); // x handle example: @username
|
||||
|
||||
/// Resolves a Twitter handle to a list of ParsedAddress objects by extracting potential cryptocurrency addresses
|
||||
/// from the user's, profile.
|
||||
/// fallback to resolving Unstoppable Domains if no addresses are found in each of the Twitter user's bio, location, and pinned tweet.
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
required String query,
|
||||
required List<CryptoCurrency> currencies,
|
||||
required WalletBase wallet,
|
||||
}) async {
|
||||
final formattedName = query.replaceFirst("@", "");
|
||||
try {
|
||||
final result = <CryptoCurrency, String>{};
|
||||
|
||||
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
|
||||
|
||||
final bio = (text: twitterUser.description, whitespaces: true);
|
||||
final location = (text: twitterUser.location, whitespaces: false);
|
||||
final pinnedTweet = (text: twitterUser.pinnedTweet?.text ?? '', whitespaces: true);
|
||||
|
||||
final twitterTexts = [
|
||||
bio,
|
||||
location,
|
||||
pinnedTweet,
|
||||
].where((item) => item.text.isNotEmpty);
|
||||
|
||||
for (final item in twitterTexts) {
|
||||
result.addAll(
|
||||
AddressResolverUtils.extractAddressesFromText(
|
||||
raw: item.text,
|
||||
currencies: currencies,
|
||||
requireSurroundingWhitespaces: item.whitespaces,
|
||||
),
|
||||
);
|
||||
|
||||
if (result.isEmpty) {
|
||||
final domainResult = await AddressResolverUtils.resolveUnstoppableDomainFromText(
|
||||
raw: item.text,
|
||||
currencies: currencies,
|
||||
);
|
||||
|
||||
if (domainResult.isNotEmpty) {
|
||||
return [_buildTwitterParsedAddress(query, twitterUser, domainResult)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
return [_buildTwitterParsedAddress(query, twitterUser, result)];
|
||||
} catch (e) {
|
||||
printV('[address resolver service] Twitter error: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
ParsedAddress _buildTwitterParsedAddress(
|
||||
String query,
|
||||
TwitterUser twitterUser,
|
||||
Map<CryptoCurrency, String> result,
|
||||
) {
|
||||
return ParsedAddress(
|
||||
parsedAddressByCurrencyMap: result,
|
||||
addressSource: AddressSource.twitter,
|
||||
handle: query,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/core/address_resolver/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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user