mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-29 06:38:45 +01:00
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.
261 lines
7.2 KiB
Dart
261 lines
7.2 KiB
Dart
import "dart:async";
|
|
import 'dart:convert';
|
|
|
|
import "package:hash_wallet/entities/saved_nfts.dart";
|
|
import 'package:hash_wallet/entities/solana_nft_asset_model.dart';
|
|
import 'package:hash_wallet/generated/i18n.dart';
|
|
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
|
import 'package:hash_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
|
import 'package:hash_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
|
|
import 'package:hash_wallet/evm/evm.dart';
|
|
import 'package:hash_wallet/utils/debounce.dart';
|
|
import "package:cw_core/utils/print_verbose.dart";
|
|
import 'package:cw_core/wallet_type.dart';
|
|
import 'package:cw_core/utils/proxy_wrapper.dart';
|
|
import 'package:mobx/mobx.dart';
|
|
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
|
|
|
import 'package:hash_wallet/entities/wallet_nft_response.dart';
|
|
import 'package:hash_wallet/store/app_store.dart';
|
|
|
|
part 'nft_view_model.g.dart';
|
|
|
|
class NFTViewModel = NFTViewModelBase with _$NFTViewModel;
|
|
|
|
abstract class NFTViewModelBase with Store {
|
|
NFTViewModelBase(this.appStore, this.bottomSheetService)
|
|
: isLoading = false,
|
|
isImportNFTLoading = false,
|
|
nftAssetByWalletModels = ObservableList(),
|
|
solanaNftAssetModels = ObservableList() {
|
|
if (isEVMCompatibleChain(appStore.wallet!.type)) {
|
|
reaction((_) {
|
|
final wallet = appStore.wallet;
|
|
if (wallet != null) return wallet.chainId;
|
|
|
|
return null;
|
|
}, (_) => _fetchDebounce.run(() => getNFTAssetByWallet()));
|
|
}
|
|
}
|
|
|
|
final AppStore appStore;
|
|
bool _nftErrorPopupShown = false;
|
|
final BottomSheetService bottomSheetService;
|
|
final Debounce _fetchDebounce = Debounce(const Duration(milliseconds: 500));
|
|
|
|
Future<void> _lastFetch = Future.value();
|
|
Completer<void>? _queuedFetch;
|
|
|
|
final SavedNFTs _savedNFTs = SavedNFTs();
|
|
int _currentRefreshId = 0;
|
|
|
|
static const _requestTimeout = Duration(seconds: 20);
|
|
|
|
Map<String, String> get _moralisHeaders => {
|
|
"Accept": "application/json",
|
|
"X-API-Key": secrets.moralisApiKey,
|
|
};
|
|
|
|
@observable
|
|
bool isLoading;
|
|
|
|
@observable
|
|
bool isImportNFTLoading;
|
|
|
|
@observable
|
|
int unresolvedNFTCount = 0;
|
|
|
|
ObservableList<NFTAssetModel> nftAssetByWalletModels;
|
|
|
|
ObservableList<SolanaNFTAssetModel> solanaNftAssetModels;
|
|
|
|
// Hash Bags: Solana is not supported.
|
|
bool get isSolanaWallet => false;
|
|
|
|
String? get currentWalletName => appStore.wallet?.name;
|
|
|
|
@action
|
|
void onWalletChanged() {
|
|
nftAssetByWalletModels.clear();
|
|
solanaNftAssetModels.clear();
|
|
unresolvedNFTCount = 0;
|
|
_nftErrorPopupShown = false;
|
|
_fetchDebounce.run(getNFTAssetByWallet);
|
|
}
|
|
|
|
@action
|
|
Future<void> getNFTAssetByWallet() {
|
|
final queued = _queuedFetch;
|
|
|
|
if (queued != null) {
|
|
return queued.future;
|
|
}
|
|
|
|
final done = Completer<void>();
|
|
_queuedFetch = done;
|
|
|
|
final waitFor = _lastFetch;
|
|
_lastFetch = done.future;
|
|
|
|
unawaited(_fetchAfterPreviousRun(waitFor, done));
|
|
|
|
return done.future;
|
|
}
|
|
|
|
Future<void> _fetchAfterPreviousRun(Future<void> waitFor, Completer<void> done) async {
|
|
await waitFor;
|
|
|
|
if (identical(_queuedFetch, done)) {
|
|
_queuedFetch = null;
|
|
}
|
|
|
|
try {
|
|
final wallet = appStore.wallet!;
|
|
|
|
if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) {
|
|
return;
|
|
}
|
|
|
|
final walletAddress = wallet.walletInfo.address;
|
|
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: wallet.chainId);
|
|
isLoading = true;
|
|
|
|
final refreshId = ++_currentRefreshId;
|
|
|
|
await _getEvmNFTAssets(wallet.name, walletAddress, chainName, refreshId);
|
|
|
|
_nftErrorPopupShown = false;
|
|
} catch (e) {
|
|
printV("Fetching wallet NFTs failed: ${e.toString()}");
|
|
if (!_nftErrorPopupShown) {
|
|
_nftErrorPopupShown = true;
|
|
bottomSheetService.queueBottomSheet(
|
|
isModalDismissible: true,
|
|
widget: BottomSheetMessageDisplayWidget(
|
|
message: S.current.moralis_nft_error,
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
isLoading = false;
|
|
done.complete();
|
|
}
|
|
}
|
|
|
|
Future<void> _getEvmNFTAssets(
|
|
String walletName,
|
|
String walletAddress,
|
|
String chainName,
|
|
int refreshId,
|
|
) async {
|
|
final saved = await _savedNFTs.evmNFTs(walletName, chainName);
|
|
|
|
var result = const <NFTAssetModel>[];
|
|
Object? indexerError;
|
|
|
|
try {
|
|
final response = await ProxyWrapper()
|
|
.get(
|
|
clearnetUri: Uri.https(
|
|
"deep-index.moralis.io",
|
|
"/api/v2.2/$walletAddress/nft",
|
|
{
|
|
"chain": chainName,
|
|
"format": "decimal",
|
|
"media_items": "false",
|
|
"exclude_spam": "true",
|
|
"normalizeMetadata": "true",
|
|
},
|
|
),
|
|
headers: _moralisHeaders,
|
|
)
|
|
.timeout(_requestTimeout);
|
|
|
|
result = WalletNFTsResponseModel.fromJson(
|
|
jsonDecode(response.body) as Map<String, dynamic>,
|
|
).result ??
|
|
[];
|
|
} catch (e) {
|
|
indexerError = e;
|
|
}
|
|
|
|
if (appStore.wallet?.walletInfo.address != walletAddress || refreshId != _currentRefreshId) {
|
|
return;
|
|
}
|
|
|
|
final listed = result.map(SavedNFTs.evmIdentifier).toSet();
|
|
|
|
nftAssetByWalletModels.clear();
|
|
nftAssetByWalletModels.addAll(result);
|
|
nftAssetByWalletModels
|
|
.addAll(saved.where((asset) => !listed.contains(SavedNFTs.evmIdentifier(asset))));
|
|
|
|
if (indexerError != null) {
|
|
throw indexerError;
|
|
}
|
|
}
|
|
|
|
@action
|
|
Future<void> onNFTSent(String walletName, String mint) async {
|
|
await _savedNFTs.removeSolana(walletName, mint);
|
|
|
|
if (appStore.wallet?.name == walletName) {
|
|
solanaNftAssetModels.removeWhere((asset) => asset.mint == mint);
|
|
}
|
|
}
|
|
|
|
Future<void> _importEvmNFT(
|
|
String walletName,
|
|
String tokenAddress,
|
|
String? tokenId,
|
|
String chainName,
|
|
) async {
|
|
final response = await ProxyWrapper()
|
|
.get(
|
|
clearnetUri: Uri.https(
|
|
"deep-index.moralis.io",
|
|
"/api/v2.2/nft/$tokenAddress/$tokenId",
|
|
{
|
|
"chain": chainName,
|
|
"format": "decimal",
|
|
"media_items": "false",
|
|
"normalizeMetadata": "true",
|
|
},
|
|
),
|
|
headers: _moralisHeaders,
|
|
)
|
|
.timeout(_requestTimeout);
|
|
|
|
final asset = NFTAssetModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
|
|
nftAssetByWalletModels.add(asset);
|
|
|
|
await _savedNFTs.addEvm(walletName, chainName, asset);
|
|
}
|
|
|
|
@action
|
|
Future<void> importNFT(String tokenAddress, String? tokenId) async {
|
|
final wallet = appStore.wallet!;
|
|
int? chainId;
|
|
if (isEVMCompatibleChain(wallet.type)) {
|
|
chainId = evm!.getSelectedChainId(wallet);
|
|
}
|
|
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: chainId);
|
|
try {
|
|
isImportNFTLoading = true;
|
|
|
|
await _importEvmNFT(wallet.name, tokenAddress, tokenId, chainName);
|
|
} catch (e) {
|
|
printV("Importing NFT $tokenAddress failed: ${e.toString()}");
|
|
bottomSheetService.queueBottomSheet(
|
|
isModalDismissible: true,
|
|
widget: BottomSheetMessageDisplayWidget(
|
|
message: S.current.nft_import_failed,
|
|
),
|
|
);
|
|
} finally {
|
|
isImportNFTLoading = false;
|
|
}
|
|
}
|
|
}
|