From aa1dab4b1a4e9a379e3afe452a7944b15423a506 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Thu, 16 Jul 2026 13:42:31 +0200 Subject: [PATCH 1/5] fix tokens button appearing on litecoin (#3405) --- .../coins_page/assets_history/assets_history_section.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart index b987c11e2..d36d67bf3 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart @@ -49,6 +49,7 @@ class _AssetsHistorySectionState extends State { final oldTabLength = tabs.length; final hasAssetsTab = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled || (widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled); + final hasAssetsButton = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled; final hasNftTab = isNFTACtivatedChain(widget.dashboardViewModel.wallet.type, chainId: widget.dashboardViewModel.wallet.chainId); tabs = [ @@ -58,13 +59,14 @@ class _AssetsHistorySectionState extends State { AssetsSection( dashboardViewModel: widget.dashboardViewModel, ), + hasAssetsButton ? AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg", () { Navigator.of(context).pushNamed( Routes.homeSettings, arguments: widget.dashboardViewModel.balanceViewModel, ); - })), + }) : null), AssetsHistorySectionTab( S.current.history, HistorySection( From 0822c7d976c0d5a9daa18a69da3e12655206028a Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:57:23 +0100 Subject: [PATCH 2/5] CW-1548: Solana wallet performance improvements (#3404) * fix android CI * add incremental sync, parallel fetch for native and spl token, and other improvments on perf and display regarding lagging --- cw_core/lib/amount/money.dart | 4 + cw_core/lib/format_fixed.dart | 10 +- cw_core/lib/parse_fixed.dart | 7 +- cw_solana/lib/solana_client.dart | 149 ++++++++++++------ cw_solana/lib/solana_transaction_history.dart | 40 +++-- cw_solana/lib/solana_wallet.dart | 135 ++++++++++++---- lib/solana/cw_solana.dart | 19 ++- .../dashboard/balance_view_model.dart | 2 - .../dashboard/dashboard_view_model.dart | 14 +- .../dashboard/transaction_list_item.dart | 12 +- 10 files changed, 284 insertions(+), 108 deletions(-) diff --git a/cw_core/lib/amount/money.dart b/cw_core/lib/amount/money.dart index 3ff5af928..9c2332310 100644 --- a/cw_core/lib/amount/money.dart +++ b/cw_core/lib/amount/money.dart @@ -191,6 +191,10 @@ class Money implements Comparable { @override int get hashCode => amount.hashCode ^ currency.hashCode; + // Added this to reduce the hops we do to convert Money to double + // for fiat conversion and display + double toDouble() => amount / multiplierOf(currency.decimals); + @override String toString() => formatFixed(amount, currency.decimals); diff --git a/cw_core/lib/format_fixed.dart b/cw_core/lib/format_fixed.dart index 52c0e1470..aa0162812 100644 --- a/cw_core/lib/format_fixed.dart +++ b/cw_core/lib/format_fixed.dart @@ -4,13 +4,11 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri decimals ??= 0; fractionalDigits ??= decimals; - var multiplier = getMultiplier(decimals); - // Make sure wei is a big number (convert as necessary) + final multiplier = multiplierOf(decimals); var negative = value.isNegative; - if (negative) value = value * BigInt.from(-1); + if (negative) value = -value; - var fraction = - value.modPow(BigInt.one, BigInt.parse(multiplier)).toString().padLeft(decimals, "0"); + var fraction = (value % multiplier).toString().padLeft(decimals, "0"); if (fractionalDigits < 0) fractionalDigits = 0; if (fractionalDigits > decimals) fractionalDigits = decimals; @@ -20,7 +18,7 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri fraction = removeTrailing("0", fraction); } - final whole = (value ~/ BigInt.parse(multiplier)); + final whole = value ~/ multiplier; final valString = fraction.isEmpty ? "$whole" : "$whole.$fraction"; diff --git a/cw_core/lib/parse_fixed.dart b/cw_core/lib/parse_fixed.dart index 76c74dc9f..dbbc24f7c 100644 --- a/cw_core/lib/parse_fixed.dart +++ b/cw_core/lib/parse_fixed.dart @@ -58,7 +58,7 @@ BigInt parseFixed(String value, int decimals) { final wholeValue = BigInt.parse(whole); final fractionValue = BigInt.parse(fraction); - final multiplierValue = BigInt.parse(multiplier); + final multiplierValue = multiplierOf(decimals); var wei = (wholeValue * multiplierValue) + fractionValue; @@ -69,3 +69,8 @@ BigInt parseFixed(String value, int decimals) { // Returns a string "1" followed by decimal "0"s String getMultiplier(int decimals) => "1".padRight(decimals + 1, "0"); + +final _multipliers = {}; + +// this is more direct and faster than having it as string then parsing everytime to get the number +BigInt multiplierOf(int decimals) => _multipliers[decimals] ??= BigInt.from(10).pow(decimals); diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index b166a3505..ef1799cfa 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -32,14 +32,29 @@ class TransactionFetchResult { }); } +class TransactionSyncResult { + final List transactions; + final String? newestSignature; + + TransactionSyncResult({ + required this.transactions, + this.newestSignature, + }); +} + class SolanaWalletClient { // Minimum amount in SOL to consider a transaction valid (to filter spam) static Money minValidAmount = Money.parse("0.00000003", CryptoCurrency.sol); + + static const int _signaturePageSize = 1000; + late final client = ProxyWrapper().getHttpIOClient(); SolanaRPC? _provider; + bool _isStopped = false; bool connect(Node node) { try { + _isStopped = false; String formattedUrl; String protocolUsed = node.isSSL ? "https" : "http"; @@ -625,10 +640,7 @@ class SolanaWalletClient { incomingAmount = diff.toDouble(); incomingMintAddress = mint; final token = await getTokenInfo(mint); - printV(token?.symbol); - printV(token?.decimals); - incomingToken = - token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); + incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); incomingTo = walletAddress; // We find the intermediate account if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) { @@ -951,27 +963,67 @@ class SolanaWalletClient { return mints.toList(); } - /// Load the Address's transactions into the account - Future> fetchTransactions( + Future>> _getAllSignaturesSinceLastFetch( + SolAddress address, + String? until, + Commitment? commitment, + ) async { + final signatures = >[]; + String? before; + + while (true) { + final currentPageSignatureResults = await _provider!.request( + SolanaRPCGetSignaturesForAddress( + account: address, + commitment: commitment, + until: until, + before: before, + limit: _signaturePageSize, + ), + ); + + if (currentPageSignatureResults.isEmpty) break; + + signatures.addAll(currentPageSignatureResults); + + if (currentPageSignatureResults.length < _signaturePageSize) break; + + if (until == null) break; + + final lastSignatureOnPage = currentPageSignatureResults.last['signature'] as String; + + if (lastSignatureOnPage == before) break; + + before = lastSignatureOnPage; + } + + return signatures; + } + + Future fetchTransactions( SolAddress address, { SPLToken? splToken, Commitment? commitment, SolAddress? walletAddress, + String? untilSignature, required void Function(List) onUpdate, }) async { - List transactions = []; + final transactions = []; + try { - final signatures = await _provider!.request( - SolanaRPCGetSignaturesForAddress( - account: address, - commitment: commitment, - ), - ); + final signatures = + await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment); + + if (signatures.isEmpty) return TransactionSyncResult(transactions: transactions); // The maximum concurrent batch size. const int batchSize = 10; + bool hasFailures = false; + for (int i = 0; i < signatures.length; i += batchSize) { + if (_isStopped) return TransactionSyncResult(transactions: transactions); + final batch = signatures.skip(i).take(batchSize).toList(); final batchResponses = await Future.wait(batch.map((signature) async { @@ -985,6 +1037,7 @@ class SolanaWalletClient { ), ); } catch (e) { + hasFailures = true; return null; } })); @@ -999,16 +1052,16 @@ class SolanaWalletClient { final parsedTransactionsLists = await Future.wait(parsedTransactionsFutures); - // We flatten the list of lists into a single list + final batchTransactions = []; for (final parsedList in parsedTransactionsLists) { if (parsedList != null) { - transactions.addAll(parsedList); + batchTransactions.addAll(parsedList); } } - // Only update UI if we have new valid transactions - if (transactions.isNotEmpty) { - onUpdate(List.from(transactions)); + if (batchTransactions.isNotEmpty) { + transactions.addAll(batchTransactions); + onUpdate(batchTransactions); } if (i + batchSize < signatures.length) { @@ -1016,38 +1069,53 @@ class SolanaWalletClient { } } - return transactions; + return TransactionSyncResult( + transactions: transactions, + newestSignature: hasFailures ? null : signatures.first['signature'] as String, + ); } catch (err, s) { printV('Error fetching transactions: $err \n$s'); - return []; + return TransactionSyncResult(transactions: transactions); } } - Future> getSPLTokenTransfers({ + final Map associatedTokenAccountCache = {}; + + Future getSPLTokenTransfers({ required String mintAddress, required SPLToken splToken, required SolanaPrivateKey privateKey, + String? untilSignature, required void Function(List) onUpdate, }) async { - ProgramDerivedAddress? associatedTokenAccount; final ownerWalletAddress = privateKey.publicKey().toAddress(); - try { - associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( - payerPrivateKey: privateKey, - mintAddress: SolAddress(mintAddress), - ownerAddress: ownerWalletAddress, - shouldCreateATA: false, - ); - } catch (e, s) { - printV('$e \n $s'); - } - if (associatedTokenAccount == null) return []; + var associatedTokenAccount = associatedTokenAccountCache[mintAddress]; + + if (associatedTokenAccount == null) { + try { + associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( + payerPrivateKey: privateKey, + mintAddress: SolAddress(mintAddress), + ownerAddress: ownerWalletAddress, + shouldCreateATA: false, + ); + } catch (e, s) { + printV('$e \n $s'); + } + + if (associatedTokenAccount == null) { + return TransactionSyncResult(transactions: []); + } + + associatedTokenAccountCache[mintAddress] = associatedTokenAccount; + } return fetchTransactions( associatedTokenAccount.address, splToken: splToken, walletAddress: ownerWalletAddress, + untilSignature: untilSignature, onUpdate: onUpdate, ); } @@ -1055,16 +1123,9 @@ class SolanaWalletClient { final Map tokenInfoCache = {}; Future getTokenInfo(String mintAddress) async { - if (tokenInfoCache.containsKey(mintAddress)) { - printV("Cached"); - return tokenInfoCache[mintAddress]; - } else { - final token = await fetchSPLTokenInfo(mintAddress); - if (token != null) { - tokenInfoCache[mintAddress] = token; - } - return token; - } + if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress]; + + return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress); } Future fetchSPLTokenInfo(String mintAddress) async { @@ -1139,7 +1200,7 @@ class SolanaWalletClient { } } - void stop() {} + void stop() => _isStopped = true; SolanaRPC? get getSolanaProvider => _provider; diff --git a/cw_solana/lib/solana_transaction_history.dart b/cw_solana/lib/solana_transaction_history.dart index 62b9db8f7..00fcbec90 100644 --- a/cw_solana/lib/solana_transaction_history.dart +++ b/cw_solana/lib/solana_transaction_history.dart @@ -31,18 +31,36 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase _saveQueue = Future.value(); + @override - Future save() async { - try { - final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); - final path = '$dirPath/$transactionsHistoryFileName'; - final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson())); - final data = json.encode({'transactions': transactionMaps}); - await encryptionFileUtils.write(path: path, password: _password, data: data); - } catch (e, s) { - printV('Error while saving solana transaction history: ${e.toString()}'); - printV(s); - } + Future save() => saveAndConfirm(); + + Future saveAndConfirm() { + final write = _saveQueue.then((_) async { + try { + await _write(); + + return true; + } catch (e, s) { + printV('Error while saving solana transaction history: ${e.toString()}'); + printV(s); + + return false; + } + }); + + _saveQueue = write; + + return write; + } + + Future _write() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson())); + final data = json.encode({'transactions': transactionMaps}); + await encryptionFileUtils.write(path: path, password: _password, data: data); } @override diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 94160686e..2305f27fb 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -88,6 +88,8 @@ abstract class SolanaWalletBase Timer? _transactionsUpdateTimer; + Future? _currentRefresh; + late final Box splTokensBox; @override @@ -321,6 +323,33 @@ abstract class SolanaWalletBase ]); } + static const _nativeSource = 'native'; + + String _lastSyncedSignatureKey(String source) => + 'solana_last_synced_signature_${walletInfo.name}_$source'; + + Future _lastSyncedSignature(String source) async { + if (transactionHistory.transactions.isEmpty) return null; + + final prefs = await _sharedPrefs.future; + + return prefs.getString(_lastSyncedSignatureKey(source)); + } + + Future _saveLastSyncedSignature(String source, String? signature) async { + if (signature == null) return; + + final prefs = await _sharedPrefs.future; + + await prefs.setString(_lastSyncedSignatureKey(source), signature); + } + + Future _clearLastSyncedSignature(String source) async { + final prefs = await _sharedPrefs.future; + + await prefs.remove(_lastSyncedSignatureKey(source)); + } + /// Polls for a specific transaction by signature with exponential backoff /// I'm using this in case we make the call to fetch the transaction and it has not finished its confirmations on the solana network and been indexed by the node networks we use. Future pollForTransaction({ @@ -365,20 +394,34 @@ abstract class SolanaWalletBase await updateTransactionsHistory(); } - void updateTransactions(List updatedTx) { - addTransactionsToTransactionHistory(updatedTx); - } + void updateTransactions(List updatedTx) => _addTransactions(updatedTx); /// Fetches the native SOL transactions linked to the wallet Public Key Future _updateNativeSOLTransactions() async { - final transactions = - await _client.fetchTransactions(_solanaPublicKey.toAddress(), onUpdate: updateTransactions); + final result = await _client.fetchTransactions( + _solanaPublicKey.toAddress(), + untilSignature: await _lastSyncedSignature(_nativeSource), + onUpdate: updateTransactions, + ); - await addTransactionsToTransactionHistory(transactions); + await _updateStateWhenSyncForTheSourceEnds(_nativeSource, result); + } + + Future _updateStateWhenSyncForTheSourceEnds( + String source, + TransactionSyncResult result, + ) async { + if (result.transactions.isNotEmpty) { + final isSaved = await transactionHistory.saveAndConfirm(); + + if (!isSaved) return; + } + + await _saveLastSyncedSignature(source, result.newestSignature); } Future updateSPLTokenTransactions({List? specificMints}) async { - final allTokens = balance.keys.whereType().toList(growable: false); + final allTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false); // Filter to specific mints if provided final tokens = specificMints != null @@ -394,30 +437,28 @@ abstract class SolanaWalletBase i, i + batchSize > tokens.length ? tokens.length : i + batchSize, ); - final results = await Future.wait( + + await Future.wait( batch.map((token) async { try { - return await _client.getSPLTokenTransfers( + final result = await _client.getSPLTokenTransfers( mintAddress: token.mintAddress, splToken: token, privateKey: _solanaPrivateKey, + untilSignature: await _lastSyncedSignature(token.mintAddress), onUpdate: updateTransactions, ); - } catch (_) { - return []; + + await _updateStateWhenSyncForTheSourceEnds(token.mintAddress, result); + } catch (e) { + printV('Error fetching spl token (${token.symbol}) transfers ${e.toString()}'); } }), ); - - for (final list in results) { - await addTransactionsToTransactionHistory(list); - } } } - Future addTransactionsToTransactionHistory( - List transactions, - ) async { + void _addTransactions(List transactions) { final Map result = {}; for (var transactionModel in transactions) { @@ -436,6 +477,12 @@ abstract class SolanaWalletBase } transactionHistory.addMany(result); + } + + Future addTransactionsToTransactionHistory( + List transactions, + ) async { + _addTransactions(transactions); await transactionHistory.save(); } @@ -456,6 +503,17 @@ abstract class SolanaWalletBase await transactionHistory.save(); } + // we want to handle the case where multiple refresh triggers (our users can swipe down + // multiple times), so we track the currrent refresh and join it instead of starting + // another one + Future _refresh() { + return _currentRefresh ??= Future.wait([ + updateTokenBalance(), + updateTransactionsHistory(), + _getEstimatedFees(), + ]).whenComplete(() => _currentRefresh = null); + } + @action @override Future startSync() async { @@ -469,12 +527,7 @@ abstract class SolanaWalletBase return; } - await Future.wait([ - updateTokenBalance(), - _updateNativeSOLTransactions(), - updateSPLTokenTransactions(), - _getEstimatedFees(), - ]); + await _refresh(); syncStatus = SyncedSyncStatus(); } catch (e) { @@ -626,6 +679,14 @@ abstract class SolanaWalletBase List get splTokenCurrencies => splTokensBox.values.toList(); + SPLToken? splTokenBySymbol(String symbol) { + for (final token in splTokensBox.values) { + if (token.symbol == symbol) return token; + } + + return null; + } + void addInitialTokens() { final initialSPLTokens = DefaultSPLTokens().initialSPLTokens; @@ -802,12 +863,27 @@ abstract class SolanaWalletBase } Future deleteSPLToken(SPLToken token) async { + final sources = {token.mintAddress}; + + if (token.symbol == CryptoCurrency.sol.symbol) { + sources.add(_nativeSource); + } + if (splTokensBox.isOpen) { + sources.addAll(splTokensBox.values + .where((t) => t.symbol == token.symbol) + .map((t) => t.mintAddress)); + await splTokensBox.delete(token.mintAddress); } balance.remove(token); await _removeTokenTransactionsInHistory(token); + + for (final source in sources) { + await _clearLastSyncedSignature(source); + } + updateTokenBalance(); } @@ -831,11 +907,12 @@ abstract class SolanaWalletBase _transactionsUpdateTimer!.cancel(); } - _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) { - updateTokenBalance(); - _updateNativeSOLTransactions(); - updateSPLTokenTransactions(); - _getEstimatedFees(); + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) async { + try { + await _refresh(); + } catch (e) { + printV('Error on periodic solana refresh: $e'); + } }); } diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index f1450331b..58999d0e5 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -122,9 +122,13 @@ class CWSolana extends Solana { return CryptoCurrency.sol; } - return (wallet as SolanaWallet).splTokenCurrencies.firstWhere( - (element) => transaction.amount.currency.symbol == element.symbol, - ); + final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol); + + if (token == null) { + throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}'); + } + + return token; } @override @@ -401,6 +405,15 @@ class CWSolana extends Solana { } await Future.wait(tokenChecks); + + final discoveredMints = result.newTokens + .where((item) => item.token.enabled) + .map((item) => item.token.mintAddress) + .toList(); + + if (discoveredMints.isNotEmpty) { + await wallet.updateSPLTokenTransactions(specificMints: discoveredMints); + } } catch (_) {} } diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 4f098c02f..3128f74d8 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -14,7 +14,6 @@ import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -498,7 +497,6 @@ abstract class BalanceViewModelBase with Store { final record = wallet.balance[curr]!; final available = record.available - (record.secondAvailable ?? Money.zero(curr)); final price = fiatConversionStore.prices[curr] ?? 0; - printV(record.available); ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString()) .replaceAll(",", "")) ?? 0; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 4dbc82752..17ffd334a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -227,9 +227,10 @@ abstract class DashboardViewModelBase with Store { 1; } catch (_) {} } else { - confirmations = appStore.wallet!.transactionHistory.transactions.values - .map((item) => item.isPending) - .fold(0, (val, pending) => pending ? val + 1 : val); + final pendingCount = appStore.wallet!.transactionHistory.transactions.values + .where((item) => item.isPending) + .length; + confirmations = pendingCount + 1; } return length * confirmations; }, _transactionDisposerCallback, delay: 300); @@ -1331,9 +1332,10 @@ abstract class DashboardViewModelBase with Store { 1; } catch (_) {} } else { - confirmations = appStore.wallet!.transactionHistory.transactions.values - .map((item) => item.isPending) - .fold(0, (val, pending) => pending ? val + 1 : val); + final pendingCount = appStore.wallet!.transactionHistory.transactions.values + .where((item) => item.isPending) + .length; + confirmations = pendingCount + 1; } return length * confirmations; }, _transactionDisposerCallback, delay: 300); diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 8676cf86c..4d6a0d527 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -195,7 +195,7 @@ class TransactionListItem extends ActionListItem with Keyable { case WalletType.decred: case WalletType.zcash: amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); case WalletType.ethereum: @@ -206,15 +206,15 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = assetOfTransaction; final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.solana: - final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); + final asset = assetOfTransaction; final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; @@ -222,7 +222,7 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; @@ -234,7 +234,7 @@ class TransactionListItem extends ActionListItem with Keyable { } final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; From ed540bae04e3f0c7e911de14a33402ae5d8b36ee Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 16 Jul 2026 20:39:49 +0200 Subject: [PATCH 3/5] CW-1291-ledger-fixes (#3390) * feat: add connecting indicator in hardware wallet connect screen * chore: bump ledger related dependencies * chore: bump ledger related dependencies * feat: add additional guard against gLedger being null * auto-reformat * feat: improve ledger connection and state management * fix: connect_device_page padding on iOS * chore: update universal_ble dependency reference --------- Co-authored-by: Robert Malikowski Co-authored-by: Omar Hatem --- lib/di.dart | 11 +- lib/monero/cw_monero.dart | 3 + .../on_authentication_state_change.dart | 35 +- .../connect_device/connect_device_page.dart | 357 ++++++++++-------- lib/store/app_store.dart | 47 +-- .../hardware_wallet/bitbox_view_model.dart | 15 +- .../hardware_wallet_view_model.dart | 13 +- .../hardware_wallet/ledger_view_model.dart | 188 +++++---- .../trezor_connect_view_model.dart | 12 +- .../wallet_list/wallet_list_view_model.dart | 3 +- pubspec_base.yaml | 14 +- tool/configure.dart | 1 + 12 files changed, 417 insertions(+), 282 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 1415bf045..6bb8bd98c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -421,16 +421,19 @@ Future setup({ case HardwareWalletType.seedsigner: case HardwareWalletType.keystone: throw Exception( - "This should not have happened, because airgapped Wallets don't need View Models"); + "This should not have happened, because airgapped Wallets don't need View Models", + ); } }); - getIt.registerLazySingleton(() => LedgerViewModel()); + getIt.registerLazySingleton(() => LedgerViewModel(getIt())); - getIt.registerLazySingleton(() => BitboxViewModel()); + getIt.registerLazySingleton(BitboxViewModel.new); getIt.registerLazySingleton( - () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet")); + () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"), + ); + getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt())); getIt.registerFactory(() => KeyService(getIt.get())); diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index e6ddabaec..1551b95de 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -433,6 +433,9 @@ class CWMonero extends Monero { @override void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection; + @override + bool hasGlobalLedgerConnection() => gLedger != null; + @override String? getLastLedgerCommand() => latestLedgerCommand; diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 49a3c61c9..02ddd5639 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -75,6 +75,23 @@ void startAuthenticationStateChange( ); return; } + + ledgerVM.suppressMoneroReconnectPrompt = true; + + if (!monero!.hasGlobalLedgerConnection()) { + await showPopUp( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: "Unexpected Error", + alertContent: + "Unexpected Error while trying to connect to the device (has no global LedgerConnection)", + buttonText: S.of(context).try_again, + buttonAction: Navigator.of(context).pop, + ), + ); + return; + } + showPopUp( context: context, builder: (context) => AlertWithOneAction( @@ -85,14 +102,17 @@ void startAuthenticationStateChange( buttonAction: () => Navigator.of(context).pop(), ), ); - bool tryOpening = true; + + var isConnected = false; + var tryOpening = true; while (tryOpening) { try { await loadCurrentWallet(); + isConnected = true; tryOpening = false; } on Exception catch (e) { final ledgerErrorMessage = ledgerVM.interpretErrorCode(e.toString()); - if (ledgerErrorMessage != null) { + if (ledgerErrorMessage != null && context.mounted) { await showPopUp( context: context, builder: (context) => AlertWithTwoActions( @@ -116,16 +136,17 @@ void startAuthenticationStateChange( } getIt.get().showNext(); - await navigatorKey.currentState! - .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + if (isConnected) { + ledgerVM.suppressMoneroReconnectPrompt = false; + await navigatorKey.currentState! + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + } }, allowChangeWallet: true, ), ); - - // await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice)); } else { - navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); } WidgetsBinding.instance.addPostFrameCallback((_) { final linkViewModel = getIt.get(); diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index c96ec27bc..21cbd0292 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -1,54 +1,54 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +import "dart:io"; -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/themes/core/material_base_theme.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/base_page.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/src/widgets/primary_button.dart"; +import "package:cake_wallet/themes/core/material_base_theme.dart"; +import "package:cake_wallet/utils/responsive_layout_util.dart"; +import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel); class ConnectDevicePageParams { - final WalletType walletType; - final OnConnectDevice onConnectDevice; - final bool allowChangeWallet; - final bool isReconnect; - final HardwareWalletType hardwareWalletType; - - ConnectDevicePageParams({ + const ConnectDevicePageParams({ required this.walletType, required this.hardwareWalletType, required this.onConnectDevice, this.allowChangeWallet = false, this.isReconnect = true, }); -} -class ConnectDevicePage extends BasePage { final WalletType walletType; final OnConnectDevice onConnectDevice; final bool allowChangeWallet; final bool isReconnect; - final HardwareWalletViewModel hardwareWalletVM; + final HardwareWalletType hardwareWalletType; +} +class ConnectDevicePage extends BasePage { ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM) : walletType = params.walletType, onConnectDevice = params.onConnectDevice, allowChangeWallet = params.allowChangeWallet, isReconnect = params.isReconnect; + final WalletType walletType; + final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; + final bool isReconnect; + final HardwareWalletViewModel hardwareWalletVM; @override String get title => isReconnect @@ -60,31 +60,32 @@ class ConnectDevicePage extends BasePage { @override Widget body(BuildContext context) => PopScope( - canPop: !isReconnect, - child: ConnectDevicePageBody( - walletType, - onConnectDevice, - allowChangeWallet, - hardwareWalletVM, - currentTheme, - )); + canPop: !isReconnect, + child: ConnectDevicePageBody( + walletType, + onConnectDevice, + hardwareWalletVM, + currentTheme, + allowChangeWallet: allowChangeWallet, + ), + ); } class ConnectDevicePageBody extends StatefulWidget { + const ConnectDevicePageBody( + this.walletType, + this.onConnectDevice, + this.hardwareWalletVM, + this.currentTheme, { + this.allowChangeWallet = false, + }); + final WalletType walletType; final OnConnectDevice onConnectDevice; final bool allowChangeWallet; final HardwareWalletViewModel hardwareWalletVM; final MaterialThemeBase currentTheme; - const ConnectDevicePageBody( - this.walletType, - this.onConnectDevice, - this.allowChangeWallet, - this.hardwareWalletVM, - this.currentTheme, - ); - @override ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); } @@ -95,10 +96,10 @@ class ConnectDevicePageBodyState extends State { List get allDevices => [...bleDevices, ...usbDevices]; - late Timer? _usbRefreshTimer = null; - late Timer? _bleRefreshTimer = null; - late Timer? _bleStateTimer = null; - late StreamSubscription? _bleRefresh = null; + Timer? _usbRefreshTimer; + Timer? _bleRefreshTimer; + Timer? _bleStateTimer; + StreamSubscription? _bleRefresh; bool longWait = false; Timer? _longWaitTimer; @@ -107,19 +108,22 @@ class ConnectDevicePageBodyState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _bleStateTimer = - Timer.periodic(Duration(seconds: 1), (_) => widget.hardwareWalletVM.updateBleState()); + _bleStateTimer = Timer.periodic( + const Duration(seconds: 1), + (_) => widget.hardwareWalletVM.updateBleState(), + ); - _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); + _bleRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshBleDevices()); if (Platform.isAndroid) { - _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); + _usbRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshUsbDevices()); } if (widget.hardwareWalletVM.hasBluetooth) { - _longWaitTimer = Timer(Duration(seconds: 10), () { - if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) + _longWaitTimer = Timer(const Duration(seconds: 10), () { + if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) { setState(() => longWait = true); + } }); } }); @@ -134,22 +138,29 @@ class ConnectDevicePageBodyState extends State { _longWaitTimer?.cancel(); widget.hardwareWalletVM.stopScanning(); + _isConnectPressed = false; super.dispose(); } Future _refreshUsbDevices() async { final dev = await widget.hardwareWalletVM.getAllUsbDevices(); - if (usbDevices.length != dev.length) setState(() => usbDevices = dev); + + if (usbDevices.length != dev.length) { + setState(() => usbDevices = dev); + } } Future _refreshBleDevices() async { try { if (widget.hardwareWalletVM.isBleEnabled) { - _bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen((device) => setState(() { - bleDevices.add(device); - if (longWait) longWait = false; - })) - ..onError((Object e) { + _bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen( + (device) => setState(() { + bleDevices.add(device); + if (longWait) { + longWait = false; + } + }), + )..onError((e) { printV(e); }); _bleRefreshTimer?.cancel(); @@ -160,48 +171,66 @@ class ConnectDevicePageBodyState extends State { } } - Future _connectToDevice(HardwareWalletDevice device) async { - final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType); + var _isConnectPressed = false; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (isConnected) - widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM); - }); + Future _connectToDevice(HardwareWalletDevice device) async { + if (_isConnectPressed) { + return; + } + + _isConnectPressed = true; + try { + final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType); + _isConnectPressed = false; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (isConnected) { + widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM); + } + }); + } catch (_) { + _isConnectPressed = false; + } } String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) { + printV(deviceType); switch (deviceType) { case HardwareWalletDeviceType.ledgerNanoX: - return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; case HardwareWalletDeviceType.ledgerNanoGen5: - return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg"; case HardwareWalletDeviceType.ledgerStax: - return 'assets/new-ui/hardware_wallets/ledger_stax.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_stax.svg"; case HardwareWalletDeviceType.ledgerFlex: - return 'assets/new-ui/hardware_wallets/ledger_flex.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_flex.svg"; case HardwareWalletDeviceType.BitBox02: case HardwareWalletDeviceType.BitBox02Nova: - return 'assets/new-ui/hardware_wallets/device_bitbox.svg'; + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; case HardwareWalletDeviceType.trezorModelOne: - return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_model_one.svg"; case HardwareWalletDeviceType.trezorModelT: - return 'assets/new-ui/hardware_wallets/device_trezor_model_t.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_model_t.svg"; case HardwareWalletDeviceType.trezorSafe3: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_3.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_3.svg"; case HardwareWalletDeviceType.trezorSafe5: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg"; case HardwareWalletDeviceType.trezorSafe7: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg"; default: - return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; } } String get description { - if (!Platform.isAndroid) return S.of(context).connect_your_hardware_wallet_ble; + if (!Platform.isAndroid) { + return S.of(context).connect_your_hardware_wallet_ble; + } - if (widget.hardwareWalletVM.hasBluetooth) return S.of(context).connect_your_hardware_wallet; + if (widget.hardwareWalletVM.hasBluetooth) { + return S.of(context).connect_your_hardware_wallet; + } return S.of(context).connect_your_hardware_wallet_usb; } @@ -210,7 +239,7 @@ class ConnectDevicePageBodyState extends State { Widget build(BuildContext context) => Center( child: Container( width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, - padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24), + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24), child: Column( children: [ Expanded( @@ -218,9 +247,7 @@ class ConnectDevicePageBodyState extends State { child: Column( spacing: 24, children: [ - CakeImageWidget( - imageUrl: "assets/new-ui/hardware_wallet.svg", - ), + const CakeImageWidget(imageUrl: "assets/new-ui/hardware_wallet.svg"), Text( description, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), @@ -231,7 +258,7 @@ class ConnectDevicePageBodyState extends State { offstage: widget.hardwareWalletVM.isBleEnabled || !widget.hardwareWalletVM.hasBluetooth, child: Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), child: Text( S.of(context).ledger_please_enable_bluetooth, style: Theme.of(context).textTheme.titleMedium, @@ -242,67 +269,89 @@ class ConnectDevicePageBodyState extends State { ), Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(18)), + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18), + ), child: ListView.separated( - shrinkWrap: true, - itemBuilder: (context, index) { - final item = allDevices[index]; - return GestureDetector( - onTap: () => _connectToDevice(item), - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: 48, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - spacing: 12, - children: [ - CakeImageWidget( - imageUrl: _getDeviceTileLeading(item.type), - width: 24, - height: 24, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, - BlendMode.srcIn), - ), - Text(item.name) - ], - ), - Row( - spacing: 12, - children: [ - if (item.connectionType == - HardwareWalletConnectionType.ble) - CakeImageWidget( - imageUrl: "assets/new-ui/bluetooth.svg", + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + final item = allDevices[index]; + return GestureDetector( + onTap: () => _connectToDevice(item), + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + Observer( + builder: (_) { + if (widget.hardwareWalletVM.isConnecting) { + return CupertinoActivityIndicator( + animating: true, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + radius: 12, + ); + } + + return CakeImageWidget( + imageUrl: _getDeviceTileLeading(item.type), width: 24, height: 24, colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, - BlendMode.srcIn), - ), + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ); + }, + ), + Text(item.name), + ], + ), + Row( + spacing: 12, + children: [ + if (item.connectionType == + HardwareWalletConnectionType.ble) CakeImageWidget( - imageUrl: "assets/new-ui/arrow_forward.svg", - height: 16, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, - BlendMode.srcIn)) - ], - ) - ], - ), + imageUrl: "assets/new-ui/bluetooth.svg", + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + CakeImageWidget( + imageUrl: "assets/new-ui/arrow_forward.svg", + height: 16, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + ], + ), + ], ), ), - ); - }, - separatorBuilder: (context, index) => Container( - height: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh), - itemCount: allDevices.length), + ), + ); + }, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + itemCount: allDevices.length, + ), ), if (widget.allowChangeWallet) ...[ PrimaryButton( @@ -310,7 +359,7 @@ class ConnectDevicePageBodyState extends State { color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, onPressed: _onChangeWallet, - ) + ), ], ], ), @@ -321,7 +370,7 @@ class ConnectDevicePageBodyState extends State { color: Theme.of(context).colorScheme.surfaceContainer, textColor: Theme.of(context).colorScheme.primary, onPressed: () => _onHowToConnect(context), - ) + ), ], ), ), @@ -339,17 +388,25 @@ class ConnectDevicePageBodyState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (BuildContext bottomSheetContext) => InfoStepsBottomSheet( + builder: (_) => InfoStepsBottomSheet( titleText: S.of(context).how_to_connect, steps: [ - InfoStep('assets/images/wallet_connect_step_icons/step1_power.svg', - S.of(context).connect_hw_info_step_1), - InfoStep('assets/images/wallet_connect_step_icons/step2_connect.svg', - S.of(context).connect_hw_info_step_2), - InfoStep('assets/images/wallet_connect_step_icons/step3_unlock.svg', - S.of(context).connect_hw_info_step_3), - InfoStep('assets/images/wallet_connect_step_icons/step4_select.svg', - S.of(context).connect_hw_info_step_4), + InfoStep( + "assets/images/wallet_connect_step_icons/step1_power.svg", + S.of(context).connect_hw_info_step_1, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step2_connect.svg", + S.of(context).connect_hw_info_step_2, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step3_unlock.svg", + S.of(context).connect_hw_info_step_3, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step4_select.svg", + S.of(context).connect_hw_info_step_4, + ), ], ), ); diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index b3ac158d9..85a9bbba5 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,25 +1,25 @@ -import 'dart:async'; +import "dart:async"; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; -import 'package:cake_wallet/themes/core/theme_store.dart'; -import 'package:cake_wallet/utils/exception_handler.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_core/balance.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cake_wallet/store/wallet_list_store.dart'; -import 'package:cake_wallet/store/authentication_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import "package:cake_wallet/core/amount_parsing_proxy.dart"; +import "package:cake_wallet/di.dart"; +import "package:cake_wallet/entities/preferences_key.dart"; +import "package:cake_wallet/reactions/wallet_connect.dart"; +import "package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart"; +import "package:cake_wallet/store/authentication_store.dart"; +import "package:cake_wallet/store/settings_store.dart"; +import "package:cake_wallet/store/wallet_list_store.dart"; +import "package:cake_wallet/themes/core/theme_store.dart"; +import "package:cake_wallet/utils/exception_handler.dart"; +import "package:cw_core/balance.dart"; +import "package:cw_core/transaction_history.dart"; +import "package:cw_core/transaction_info.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:mobx/mobx.dart"; +import "package:shared_preferences/shared_preferences.dart"; -part 'app_store.g.dart'; +part "app_store.g.dart"; class AppStore = AppStoreBase with _$AppStore; @@ -59,8 +59,9 @@ abstract class AppStoreBase with Store { @action Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { - bool changingToSameWalletType = this.wallet?.type == wallet.type; - this.wallet?.close(shouldCleanup: !changingToSameWalletType); + final changingToSameWalletType = this.wallet?.type == wallet.type; + + await this.wallet?.close(shouldCleanup: !changingToSameWalletType); this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); @@ -80,7 +81,7 @@ abstract class AppStoreBase with Store { wcService.create(); await wcService.init(); } catch (e, s) { - printV('WalletConnect setup failed: $e\n$s'); + printV("WalletConnect setup failed: $e\n$s"); } } } diff --git a/lib/view_model/hardware_wallet/bitbox_view_model.dart b/lib/view_model/hardware_wallet/bitbox_view_model.dart index 39c4b5123..a07b63cfb 100644 --- a/lib/view_model/hardware_wallet/bitbox_view_model.dart +++ b/lib/view_model/hardware_wallet/bitbox_view_model.dart @@ -35,6 +35,10 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { @observable bool isBleEnabled = false; + @override + @observable + bool isConnecting = false; + @override bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth @@ -52,10 +56,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { Future stopScanning() async {} @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type) async { if (!(device is BitboxHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (isConnecting) return false; + isConnecting = true; try { final appDocDir = await getAppDir(); @@ -66,17 +71,15 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt'); await bitboxManager.channelHashVerify(); printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt'); - _isConnecting = false; + isConnecting = false; return true; } catch (e) { printV(e); } - _isConnecting = false; + isConnecting = false; return false; } - bool _isConnecting = false; - @override bool isConnected(WalletType type) => false; diff --git a/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart b/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart index 1b87abff2..ce5473914 100644 --- a/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart +++ b/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart @@ -1,13 +1,14 @@ -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; abstract class HardwareWalletViewModel { HardwareWalletType get hardwareWalletType; bool get isBleEnabled; bool get hasBluetooth; + bool get isConnecting; bool isConnected(WalletType type); @@ -26,4 +27,6 @@ abstract class HardwareWalletViewModel { Future initWallet(WalletBase wallet); String? interpretErrorCode(String error) => null; + + Future close() async {} } diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index dc608d8d3..f5efaa52e 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -1,50 +1,47 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +import "dart:io"; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cw_core/hardware/device_connection_type.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/widgets.dart'; -import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk; -import 'package:mobx/mobx.dart'; -import 'package:permission_handler/permission_handler.dart'; +import "package:cake_wallet/bitcoin/bitcoin.dart"; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cake_wallet/evm/evm.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/monero/monero.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart"; +import "package:cake_wallet/wallet_type_utils.dart"; +import "package:cw_core/hardware/device_connection_type.dart"; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter/widgets.dart"; +import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk; +import "package:mobx/mobx.dart"; +import "package:permission_handler/permission_handler.dart"; -part 'ledger_view_model.g.dart'; +part "ledger_view_model.g.dart"; class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel; abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { - late final sdk.LedgerInterface ledgerPlusBLE; - late final sdk.LedgerInterface ledgerPlusUSB; - - bool get _doesSupportHardwareWallets { - if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes( - WalletType.monero, HardwareWalletType.ledger, Platform.isIOS) - .isNotEmpty; - } - - return true; - } - - LedgerViewModelBase() { + LedgerViewModelBase(AppStore appStore) { if (_doesSupportHardwareWallets) { reaction((_) => isBleEnabled, (_) { - if (isBleEnabled) _initBLE(); + if (isBleEnabled) { + _initBLE(); + } }); + + reaction((_) => appStore.wallet, (wallet) async { + if (wallet != null && wallet.type != _connectingWalletType) { + await close(); + } + }); + updateBleState(); if (!Platform.isIOS) { @@ -53,6 +50,21 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { } } + late final sdk.LedgerInterface ledgerPlusBLE; + late final sdk.LedgerInterface ledgerPlusUSB; + + bool get _doesSupportHardwareWallets { + if (isMoneroOnly) { + return DeviceConnectionType.supportedConnectionTypes( + WalletType.monero, + HardwareWalletType.ledger, + Platform.isIOS, + ).isNotEmpty; + } + + return true; + } + @override HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger; @@ -60,6 +72,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { @observable bool isBleEnabled = false; + @override + @observable + bool isConnecting = false; + @override bool get hasBluetooth => true; @@ -69,9 +85,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { if (isBleEnabled && !_bleIsInitialized) { ledgerPlusBLE = sdk.LedgerInterface.ble( onPermissionRequest: (_) async { - if (Platform.isMacOS) return true; + if (Platform.isMacOS) { + return true; + } - Map statuses = await [ + final statuses = await [ Permission.bluetoothScan, Permission.bluetoothConnect, Permission.bluetoothAdvertise, @@ -79,7 +97,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return statuses.values.where((status) => status.isDenied).isEmpty; }, - bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)), + bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)), ); _bleIsInitialized = true; } @@ -91,28 +109,41 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { final newState = bleState == sdk.AvailabilityState.poweredOn; - if (newState != isBleEnabled) isBleEnabled = newState; + if (newState != isBleEnabled) { + isBleEnabled = newState; + } } @override Stream scanForBleDevices() => - ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d)); + ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new); @override - Future> getAllUsbDevices() => ledgerPlusUSB.devices - .then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList()); + Future> getAllUsbDevices() => + ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList()); @override Future stopScanning() async { - if (_bleIsInitialized) await ledgerPlusBLE.stopScanning(); - if (!Platform.isIOS) await ledgerPlusUSB.stopScanning(); + if (_bleIsInitialized) { + await ledgerPlusBLE.stopScanning(); + } + if (!Platform.isIOS) { + await ledgerPlusUSB.stopScanning(); + } } @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type) async { - if (!(device is LedgerHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (device is! LedgerHardwareWalletDevice) { + return false; + } + + if (isConnecting) { + return false; + } + + isConnecting = true; _connectingWalletType = type; if (isConnected(type)) { try { @@ -123,34 +154,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { final ledger = device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB; - if (_connectionChangeSubscription == null) { - _connectionChangeSubscription = - ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener); - } + _connectionChangeSubscription ??= + ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener); try { _connection = await ledger.connect(device.device); - _isConnecting = false; + isConnecting = false; return true; } catch (e) { printV(e); } - _isConnecting = false; + isConnecting = false; return false; } StreamSubscription? _connectionChangeSubscription; sdk.LedgerConnection? _connection; - bool _isConnecting = false; WalletType? _connectingWalletType; + bool suppressMoneroReconnectPrompt = false; void _connectionChangeListener(sdk.BleConnectionState event) { - printV('Ledger Device State Changed: $event'); - if (event == sdk.BleConnectionState.disconnected && !_isConnecting) { + printV("Ledger Device State Changed: $event"); + if (event == sdk.BleConnectionState.disconnected && !isConnecting) { _connection = null; if (_connectingWalletType == WalletType.monero) { monero!.resetLedgerConnection(); + if (suppressMoneroReconnectPrompt) { + return; + } + Navigator.of(navigatorKey.currentContext!).pushNamed( Routes.connectDevices, arguments: ConnectDevicePageParams( @@ -158,7 +191,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { hardwareWalletType: HardwareWalletType.ledger, allowChangeWallet: true, isReconnect: true, - onConnectDevice: (context, ledgerVM) async { + onConnectDevice: (context, ledgerVM) { if (context.mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } @@ -170,26 +203,24 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { } @override - bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected); + bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected; sdk.LedgerConnection get connection => _connection!; @override - Future initWallet(WalletBase wallet) async { + Future initWallet(WalletBase wallet) { switch (wallet.type) { case WalletType.monero: return monero!.setLedgerConnection(wallet, connection); case WalletType.bitcoin: - return bitcoin! - .setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); case WalletType.litecoin: - return bitcoin! - .setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); case WalletType.ethereum: case WalletType.polygon: - return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); default: - throw Exception('Unexpected wallet type: ${wallet.type} for ledger'); + throw Exception("Unexpected wallet type: ${wallet.type} for ledger"); } } @@ -216,16 +247,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return error; } - var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString()); + final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString()); - String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? ""; + final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? ""; if (errorCode.contains("6985")) { return S.current.ledger_error_tx_rejected_by_user; } else if (errorCode.contains("5515")) { return S.current.ledger_error_device_locked; - } else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) { + } else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) { return S.current.ledger_error_wrong_app; } return null; } + + @override + Future close() async { + try { + printV("Resetting ledger VM"); + await _connectionChangeSubscription?.cancel(); + await _connection?.disconnect().catchError((_) {}); + + _connectingWalletType = null; + _connectionChangeSubscription = null; + _connection = null; + isConnecting = false; + await stopScanning(); + } catch (_) {} + } } diff --git a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart index 7115cdf5a..c048edf7c 100644 --- a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart +++ b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart @@ -82,6 +82,10 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S @override HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor; + @override + @observable + bool isConnecting = false; + @override @observable bool isBleEnabled = false; @@ -112,7 +116,6 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S if (!Platform.isIOS) await trezorUSB.stopScanning(); } - bool _isConnecting = false; sdk.TrezorClient? _client; Completer? _pinCompleter; @@ -123,11 +126,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S void setParingPin(String pin) => _pinCompleter?.complete(pin); @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type, [bool isRetry = false]) async { if (!(device is TrezorHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (isConnecting) return false; + isConnecting = true; paringState = TrezorParingState.initial; try { @@ -182,7 +186,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S printV(e); return false; } finally { - _isConnecting = false; + isConnecting = false; } } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index d8ae44e53..78b6793dd 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -77,8 +77,7 @@ abstract class WalletListViewModelBase with Store { if (walletItem.type == WalletType.haven) { return; } - // bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type; - // await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType); + final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); await _appStore.changeCurrentWallet(wallet); updateList(); diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 90c72af2b..ee0c0f861 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -125,7 +125,7 @@ dependencies: ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus - ref: 0ccfbe26298e8974dfd4e891f34719c8348562f9 + ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28 trezor_connect: git: url: https://github.com/cake-tech/trezor_connect @@ -201,14 +201,8 @@ dependency_overrides: ffi: 2.1.0 ledger_flutter_plus: git: - url: https://github.com/cake-tech/ledger-flutter-plus - ref: 7bcb00380783f8611f68f5cd63fec96261fea2c2 - ledger_usb_plus: - git: - # url: https://github.com/konstantinullrich/ledger-usb-plus - # ref: dad482c847c0d0eedceabbb40d3478e64b65e630 - url: https://github.com/cake-tech/ledger-usb-plus - ref: 734c3d6f8267ba52ad73b6e1e78e570c07298890 + url: https://github.com/vespr-wallet/ledger-flutter-plus + ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28 socks5_proxy: git: url: https://github.com/LacticWhale/socks_dart.git @@ -231,7 +225,7 @@ dependency_overrides: universal_ble: git: url: https://github.com/cake-tech/universal_ble.git - ref: f7044558ad111a156987614971e08c583c80175c + ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/tool/configure.dart b/tool/configure.dart index 54febefbb..d6b76d756 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -475,6 +475,7 @@ WalletCredentials createMoneroNewWalletCredentials({required String name, requir Future setLedgerConnection(Object wallet, ledger.LedgerConnection connection); void resetLedgerConnection(); void setGlobalLedgerConnection(ledger.LedgerConnection connection); + bool hasGlobalLedgerConnection(); String? getLastLedgerCommand(); void setHardwareWalletService(Object wallet, HardwareWalletService service); HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection); From c10ce20c4f2c7579d114421b38405da693a68364 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 17 Jul 2026 01:24:28 +0300 Subject: [PATCH 4/5] delete solana sync keys when removing the wallet --- cw_solana/lib/solana_wallet_service.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index e374c9327..1ed55c411 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -13,6 +13,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_solana/solana_mnemonics.dart'; import 'package:cw_solana/solana_wallet.dart'; import 'package:cw_solana/solana_wallet_creation_credentials.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class SolanaWalletService extends WalletService< SolanaNewWalletCredentials, @@ -96,6 +97,11 @@ class SolanaWalletService extends WalletService< throw Exception('Wallet not found'); } await WalletInfo.delete(walletInfo); + final prefs = await SharedPreferences.getInstance(); + for (final key in prefs.getKeys().where( + (k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) { + await prefs.remove(key); + } } @override From 3070612f1509a5789c7d26af429ca472ebad62ac Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:40:30 +0100 Subject: [PATCH 5/5] fix: Wrong fiat for sent tx from swaps (#3409) * fix android CI * fix: wrong fiat amount showing for sent tx from swaps * fix: use token address to fetch fiat amount for pending send entry --- lib/evm/cw_evm.dart | 3 +++ lib/view_model/send/send_view_model.dart | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart index 0b28b7ee0..a291169ee 100644 --- a/lib/evm/cw_evm.dart +++ b/lib/evm/cw_evm.dart @@ -166,12 +166,15 @@ class CWEVM extends EVM { amount: amount, fee: fee, tokenSymbol: tokenSymbol, + exponent: exponent, direction: direction, isPending: isPending, date: date, confirmations: confirmations, to: to, from: from, + evmSignatureName: evmSignatureName, + contractAddress: contractAddress, chainId: chainId); @override diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index a566cab7d..a652619bd 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1121,6 +1121,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // it is required because evm, solana and tron don't actually save the transaction info when you send something. // instead, they rely on the tx to eventually get fetched at sync time, which can take a while if (isEVMWallet) { + final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull( + (token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(), + ); + wallet.transactionHistory.addOne(evm!.getTransactionInfo( id: pendingTransaction!.evmTxHashFromRawHex!, height: 0, @@ -1132,6 +1136,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor date: DateTime.now(), confirmations: 0, chainId: wallet.chainId ?? 0, + contractAddress: selectedToken?.contractAddress, )); }