Smoothen Jupiter Swap for Solana Wallets (#2816)
What changed, and why it matters
This commit polishes the new Jupiter DEX swap feature for Solana wallets in Cake Wallet. It makes balance updates faster, fixes a transaction-history display quirk, and tweaks how swap details are copied to the clipboard. There is no clear security bug being fixed; it reads as ordinary feature hardening and UI cleanup.
Treat as a routine feature commit, not an emergency security patch. If auditing the Jupiter integration, review how swap transactions are signed, how routerData/routerValue are validated, whether referral/fee parameters are tamper-resistant, and whether clipboard sanitization fully prevents user confusion or phishing via crafted suffixes.
Security signals we found
New DEX integration code path added (JupiterExchangeProvider / Solana)
Clipboard sanitization of transaction ID suffixes added
Balance-update and transaction-polling logic added after swap
No explicit security fix language in commit title or message
Evidence from the diff
The patch integrates and refines Jupiter swap execution for Solana: parallel SOL/SPL balance fetching, a dedicated Jupiter swap path in SendViewModel, polling for on-chain transaction confirmation, balance refresh after swaps, and stripping _incoming/_outgoing suffixes from displayed/copied transaction IDs. It also adds referral-fee configuration and improves error messaging. The diff does not show a discrete vulnerability fix; changes are functional improvements to a newly shipped DEX integration.
Changed components
cw_solana/lib/solana_wallet.dartlib/exchange/provider/jupiter_exchange_provider.dartlib/src/screens/transaction_details/transaction_details_page.dartlib/view_model/send/send_view_model.dartlib/view_model/transaction_details_view_model.dartInspect captured patch +105 / −27
diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart
index 8e78fd42..dba04a9d 100644
--- a/cw_solana/lib/solana_wallet.dart
+++ b/cw_solana/lib/solana_wallet.dart
@@ -531,9 +531,13 @@ abstract class SolanaWalletBase
}
Future<void> updateTokenBalance({List<String>? tokenMints}) async {
- balance[CryptoCurrency.sol] = await _fetchSOLBalance();
-
- await _updateSplTokenBalancesInternal(tokenMints: tokenMints);
+ // Fetch SOL and SPL token balances in parallel for better performance
+ await Future.wait([
+ _fetchSOLBalance().then((solBalance) {
+ balance[CryptoCurrency.sol] = solBalance;
+ }),
+ _updateSplTokenBalancesInternal(tokenMints: tokenMints),
+ ]);
await save();
}
diff --git a/lib/exchange/provider/jupiter_exchange_provider.dart b/lib/exchange/provider/jupiter_exchange_provider.dart
index a2bb6909..a10920b1 100644
--- a/lib/exchange/provider/jupiter_exchange_provider.dart
+++ b/lib/exchange/provider/jupiter_exchange_provider.dart
@@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
-import 'package:cake_wallet/exchange/exchange_pair.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
@@ -23,9 +22,9 @@ class JupiterExchangeProvider extends ExchangeProvider {
static const List<CryptoCurrency> _notSupported = [];
static final List<CryptoCurrency> _supportedCurrencies = CryptoCurrency.all
- .where((c) => c.tag == 'SOL' || c == CryptoCurrency.sol)
- .where((c) => !_notSupported.contains(c))
- .toList();
+ .where((c) => c.tag == 'SOL' || c == CryptoCurrency.sol)
+ .where((c) => !_notSupported.contains(c))
+ .toList();
static const _baseUrl = 'api.jup.ag';
static const _orderPath = '/ultra/v1/order';
diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart
index cbfd5286..d7894386 100644
--- a/lib/src/screens/transaction_details/transaction_details_page.dart
+++ b/lib/src/screens/transaction_details/transaction_details_page.dart
@@ -55,7 +55,11 @@ class TransactionDetailsPage extends BasePage {
return GestureDetector(
key: item.key,
onTap: () {
- Clipboard.setData(ClipboardData(text: item.value));
+ final textToCopy = item.title.toLowerCase() ==
+ S.of(context).transaction_details_transaction_id.toLowerCase()
+ ? item.value.replaceAll(RegExp(r'_(incoming|outgoing)$'), '')
+ : item.value;
+ Clipboard.setData(ClipboardData(text: textToCopy));
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
},
child: ListRow(
diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart
index 3d9c9b48..9eabb024 100644
--- a/lib/view_model/send/send_view_model.dart
+++ b/lib/view_model/send/send_view_model.dart
@@ -649,6 +649,38 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
+ // Jupiter (Solana) swap path
+ if (walletType == WalletType.solana && trade != null && provider is JupiterExchangeProvider) {
+ final swapTransactionBase64 = trade.routerData;
+ final requestId = trade.routerValue;
+ if (swapTransactionBase64?.isNotEmpty == true &&
+ requestId?.isNotEmpty == true &&
+ solana != null) {
+ try {
+ final actualFee = trade.fee ?? 0.0005;
+ // Fallback to estimate if not available
+ final fee = actualFee > 0 ? actualFee : 0.0005;
+
+ final amount = double.tryParse(trade.amount) ?? 0.0;
+
+ pendingTransaction = await solana!.signAndPrepareJupiterSwapTransaction(
+ wallet,
+ swapTransactionBase64!,
+ requestId!,
+ trade.payoutAddress ?? '',
+ amount,
+ fee,
+ );
+
+ state = ExecutedSuccessfullyState();
+ return pendingTransaction;
+ } catch (e, s) {
+ printV('Jupiter swap error: $e\n$s');
+ throw Exception('Failed to process Jupiter swap: $e');
+ }
+ }
+ }
+
// Regular flow
pendingTransaction = await wallet.createTransaction(_credentials(provider));
@@ -784,8 +816,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
if (walletType == WalletType.solana) {
Future.delayed(Duration(seconds: 1), () async {
try {
- // Updates tx history with the exact mints involved in transaction
- // Also updates balances for the tokens involved in the transaction
await solana!.pollForTransaction(
wallet,
pendingTransaction!.id,
@@ -793,9 +823,62 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
maxRetries: 5,
);
} catch (e) {
- printV('Failed to update transactions after send: $e');
+ printV('Failed to poll for transaction: $e');
}
});
+
+ // Update balances for currencies involved in swap
+ if (_currentTrade != null) {
+ Future.delayed(Duration(seconds: 2), () async {
+ try {
+ final tokenMints = <String>[];
+
+ // Extract from currency mint (skip native SOL)
+ if (_currentTrade!.from != null && _currentTrade!.from != CryptoCurrency.sol) {
+ try {
+ final fromMint = solana!.getTokenAddress(_currentTrade!.from!);
+ tokenMints.add(fromMint);
+ } catch (e) {
+ printV('Error getting from currency mint: $e');
+ }
+ }
+
+ // Extract to currency mint (skip native SOL)
+ if (_currentTrade!.to != null && _currentTrade!.to != CryptoCurrency.sol) {
+ try {
+ final toMint = solana!.getTokenAddress(_currentTrade!.to!);
+ tokenMints.add(toMint);
+ } catch (e) {
+ printV('Error getting to currency mint: $e');
+ }
+ }
+
+ if (tokenMints.isNotEmpty) {
+ solana!.updateTokenBalances(
+ wallet,
+ tokenMints: tokenMints,
+ );
+
+ // Retry after a bit more time to ensure balance is updated
+ Future.delayed(Duration(seconds: 2), () async {
+ try {
+ await solana!.updateTokenBalances(
+ wallet,
+ tokenMints: tokenMints,
+ );
+ } catch (e) {
+ printV('Error retrying balance update: $e');
+ }
+ });
+ }
+ } catch (e) {
+ printV('Failed to update balances after send: $e');
+ } finally {
+ _currentTrade = null;
+ _currentProvider = null;
+ }
+ });
+ }
}
// Immediate transaction update for EVM chains, Tron, and Nano
@@ -835,22 +918,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
_currentTrade!.txId = signature;
- if (!isSuccess) {
- _currentTrade!.stateRaw = TradeState.failed.raw;
- if (_currentTrade!.isInBox) {
- await _currentTrade!.save();
- }
- }
-
- if (isSuccess) {
- _currentTrade!.stateRaw = TradeState.completed.raw;
-
- if (_currentTrade!.isInBox) {
- await _currentTrade!.save();
- }
+ _currentTrade!.stateRaw = isSuccess ? TradeState.completed.raw : TradeState.failed.raw;
- _currentTrade = null;
- _currentProvider = null;
+ if (_currentTrade!.isInBox) {
+ await _currentTrade!.save();
}
}
diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart
index c5e49436..37e40341 100644
--- a/lib/view_model/transaction_details_view_model.dart
+++ b/lib/view_model/transaction_details_view_model.dart
@@ -555,7 +555,7 @@ abstract class TransactionDetailsViewModelBase with Store {
final _items = [
StandartListItem(
title: S.current.transaction_details_transaction_id,
- value: tx.txHash,
+ value: tx.txHash.replaceAll(RegExp(r'_(incoming|outgoing)$'), ''),
key: ValueKey('standard_list_item_transaction_details_id_key'),
),
StandartListItem(
Why this scored 21/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.