CW-1548: Solana wallet performance improvements (#3404)
What changed, and why it matters
This commit is a routine performance and reliability improvement for Solana wallets in Cake Wallet. It makes transaction syncing faster by only fetching new signatures since the last sync, batches network requests, prevents duplicate refreshes, and fixes a few small UI calculation bugs. There is no clear security vulnerability being patched, and the changes do not appear to introduce one.
No security action required. Treat as normal code review for performance and correctness. Consider verifying that `_saveLastSyncedSignature` is only updated after successful persistence to avoid skipped transactions on write failure, and that `_isStopped` is handled safely across isolates/futures.
Security signals we found
No security-relevant keywords in commit title or message
Changes are performance-oriented: incremental sync, parallel fetch, caching, refresh coalescing
No changes to key handling, seed storage, encryption, or authentication
No input parsing from untrusted sources beyond existing RPC data paths
One behavioral change: `confirmations` calculation now adds 1 to pending count, but this is UI-only
Evidence from the diff
The commit refactors Solana transaction synchronization to use incremental signature-based syncing (untilSignature/newestSignature), parallel batch fetching, a shared-prefs checkpoint for last-synced signatures, and a refresh coalescing mechanism. It also serializes transaction history writes via a queue, caches associated token accounts and token metadata, and cleans up fiat conversion calculations by adding Money.toDouble() and removing verbose logging. No cryptographic, authorization, or input-validation flaws are evident in the diff.
Changed components
cw_solana/lib/solana_client.dartcw_solana/lib/solana_transaction_history.dartcw_solana/lib/solana_wallet.dartlib/solana/cw_solana.dartcw_core/lib/amount/money.dartcw_core/lib/format_fixed.dartcw_core/lib/parse_fixed.dartlib/view_model/dashboard/balance_view_model.dartlib/view_model/dashboard/dashboard_view_model.dartlib/view_model/dashboard/transaction_list_item.dartInspect captured patch +284 / −108
diff --git a/cw_core/lib/amount/money.dart b/cw_core/lib/amount/money.dart
index 3ff5af92..9c233231 100644
--- a/cw_core/lib/amount/money.dart
+++ b/cw_core/lib/amount/money.dart
@@ -191,6 +191,10 @@ class Money implements Comparable<Money> {
@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 52c0e147..aa016281 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 76c74dc9..dbbc24f7 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 = <int, BigInt>{};
+
+// 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 b166a350..ef1799cf 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<SolanaTransactionModel> 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<List<SolanaTransactionModel>> fetchTransactions(
+ Future<List<Map<String, dynamic>>> _getAllSignaturesSinceLastFetch(
+ SolAddress address,
+ String? until,
+ Commitment? commitment,
+ ) async {
+ final signatures = <Map<String, dynamic>>[];
+ 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<TransactionSyncResult> fetchTransactions(
SolAddress address, {
SPLToken? splToken,
Commitment? commitment,
SolAddress? walletAddress,
+ String? untilSignature,
required void Function(List<SolanaTransactionModel>) onUpdate,
}) async {
- List<SolanaTransactionModel> transactions = [];
+ final transactions = <SolanaTransactionModel>[];
+
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 = <SolanaTransactionModel>[];
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<SolanaTransactionModel>.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<List<SolanaTransactionModel>> getSPLTokenTransfers({
+ final Map<String, ProgramDerivedAddress> associatedTokenAccountCache = {};
+
+ Future<TransactionSyncResult> getSPLTokenTransfers({
required String mintAddress,
required SPLToken splToken,
required SolanaPrivateKey privateKey,
+ String? untilSignature,
required void Function(List<SolanaTransactionModel>) 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: <SolanaTransactionModel>[]);
+ }
+
+ associatedTokenAccountCache[mintAddress] = associatedTokenAccount;
+ }
return fetchTransactions(
associatedTokenAccount.address,
splToken: splToken,
walletAddress: ownerWalletAddress,
+ untilSignature: untilSignature,
onUpdate: onUpdate,
);
}
@@ -1055,16 +1123,9 @@ class SolanaWalletClient {
final Map<String, SPLToken?> tokenInfoCache = {};
Future<SPLToken?> 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<SPLToken?> 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 62b9db8f..00fcbec9 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<Solan
await _load();
}
+ Future<void> _saveQueue = Future.value();
+
@override
- Future<void> 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<void> save() => saveAndConfirm();
+
+ Future<bool> 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<void> _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 94160686..2305f27f 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<void>? _currentRefresh;
+
late final Box<SPLToken> 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<String?> _lastSyncedSignature(String source) async {
+ if (transactionHistory.transactions.isEmpty) return null;
+
+ final prefs = await _sharedPrefs.future;
+
+ return prefs.getString(_lastSyncedSignatureKey(source));
+ }
+
+ Future<void> _saveLastSyncedSignature(String source, String? signature) async {
+ if (signature == null) return;
+
+ final prefs = await _sharedPrefs.future;
+
+ await prefs.setString(_lastSyncedSignatureKey(source), signature);
+ }
+
+ Future<void> _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<void> pollForTransaction({
@@ -365,20 +394,34 @@ abstract class SolanaWalletBase
await updateTransactionsHistory();
}
- void updateTransactions(List<SolanaTransactionModel> updatedTx) {
- addTransactionsToTransactionHistory(updatedTx);
- }
+ void updateTransactions(List<SolanaTransactionModel> updatedTx) => _addTransactions(updatedTx);
/// Fetches the native SOL transactions linked to the wallet Public Key
Future<void> _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<void> _updateStateWhenSyncForTheSourceEnds(
+ String source,
+ TransactionSyncResult result,
+ ) async {
+ if (result.transactions.isNotEmpty) {
+ final isSaved = await transactionHistory.saveAndConfirm();
+
+ if (!isSaved) return;
+ }
+
+ await _saveLastSyncedSignature(source, result.newestSignature);
}
Future<void> updateSPLTokenTransactions({List<String>? specificMints}) async {
- final allTokens = balance.keys.whereType<SPLToken>().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 <SolanaTransactionModel>[];
+
+ 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<void> addTransactionsToTransactionHistory(
- List<SolanaTransactionModel> transactions,
- ) async {
+ void _addTransactions(List<SolanaTransactionModel> transactions) {
final Map<String, SolanaTransactionInfo> result = {};
for (var transactionModel in transactions) {
@@ -436,6 +477,12 @@ abstract class SolanaWalletBase
}
transactionHistory.addMany(result);
+ }
+
+ Future<void> addTransactionsToTransactionHistory(
+ List<SolanaTransactionModel> 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<void> _refresh() {
+ return _currentRefresh ??= Future.wait([
+ updateTokenBalance(),
+ updateTransactionsHistory(),
+ _getEstimatedFees(),
+ ]).whenComplete(() => _currentRefresh = null);
+ }
+
@action
@override
Future<void> 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<SPLToken> 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<void> deleteSPLToken(SPLToken token) async {
+ final sources = <String>{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 f1450331..58999d0e 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 4f098c02..3128f74d 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 4dbc8275..17ffd334 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 8676cf86..4d6a0d52 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;
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.