Smoothen Jupiter Swap for Solana Wallets (#2816)

* feat: Integrate Jupiter DEX * feat: Enable internal swaps * feat: implement Jupiter swap execution api and enhance trade handling - Added executeSwap method to handle signed swap transactions via the execute endpoint. - Updated signAndPrepareJupiterSwapTransaction to include request ID and handle swap execution response. - Modified trade details to use txId instead of id for Jupiter trades. - Enhanced error handling for swap execution with user-friendly messages. - Updated sendviewmodel to manage trade state updates after transaction commitment. * fix: null error after successfully swapping * fix: Finallyyy fixed the annoying tx history issue for solana dex swaps * fix: update inputAddress to use toAddress in JupiterExchangeProvider * feat: enhance transaction handling and token balance updates - Cut down tx fetch/update time for transactions update after swapping - Added TransactionFetchResult class to hold parsed transactions and token mints. - Added pollForTransaction method to handle transaction polling with exponential backoff. - Conditionally hide the external send button based on provider type. * feat: add fees to trade object and handle null case * feat: add Jupiter referral fee and account configuration * fix: Jupiter dex swap fixes - Fetch and display balance after each completed swap - Make independent calls parallel, boosting balance display - Remove incoming or outoging tags from tx history

David Adegoke committed Jan 20, 2026 at 00:15 UTC 696052f515775f584002978e7172fc696f00e1b5
5 files changed +105 -27
cw_solana/lib/solana_wallet.dart
+7 -3
@@ -531,9 +531,13 @@ abstract class SolanaWalletBase
531 }
532
533 Future<void> updateTokenBalance({List<String>? tokenMints}) async {
534 - balance[CryptoCurrency.sol] = await _fetchSOLBalance();
535 -
536 - await _updateSplTokenBalancesInternal(tokenMints: tokenMints);
534 + // Fetch SOL and SPL token balances in parallel for better performance
535 + await Future.wait([
536 + _fetchSOLBalance().then((solBalance) {
537 + balance[CryptoCurrency.sol] = solBalance;
538 + }),
539 + _updateSplTokenBalancesInternal(tokenMints: tokenMints),
540 + ]);
541
542 await save();
543 }
lib/exchange/provider/jupiter_exchange_provider.dart
+3 -4
@@ -1,7 +1,6 @@
1 import 'dart:convert';
2
3 import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 -import 'package:cake_wallet/exchange/exchange_pair.dart';
4 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
5 import 'package:cake_wallet/exchange/limits.dart';
6 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
@@ -23,9 +22,9 @@ class JupiterExchangeProvider extends ExchangeProvider {
22 static const List<CryptoCurrency> _notSupported = [];
23
24 static final List<CryptoCurrency> _supportedCurrencies = CryptoCurrency.all
26 - .where((c) => c.tag == 'SOL' || c == CryptoCurrency.sol)
27 - .where((c) => !_notSupported.contains(c))
28 - .toList();
25 + .where((c) => c.tag == 'SOL' || c == CryptoCurrency.sol)
26 + .where((c) => !_notSupported.contains(c))
27 + .toList();
28
29 static const _baseUrl = 'api.jup.ag';
30 static const _orderPath = '/ultra/v1/order';
lib/src/screens/transaction_details/transaction_details_page.dart
+5 -1
@@ -55,7 +55,11 @@ class TransactionDetailsPage extends BasePage {
55 return GestureDetector(
56 key: item.key,
57 onTap: () {
58 - Clipboard.setData(ClipboardData(text: item.value));
58 + final textToCopy = item.title.toLowerCase() ==
59 + S.of(context).transaction_details_transaction_id.toLowerCase()
60 + ? item.value.replaceAll(RegExp(r'_(incoming|outgoing)$'), '')
61 + : item.value;
62 + Clipboard.setData(ClipboardData(text: textToCopy));
63 showBar<void>(context, S.of(context).transaction_details_copied(item.title));
64 },
65 child: ListRow(
lib/view_model/send/send_view_model.dart
+89 -18
@@ -649,6 +649,38 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
649 }
650 }
651
652 + // Jupiter (Solana) swap path
653 + if (walletType == WalletType.solana && trade != null && provider is JupiterExchangeProvider) {
654 + final swapTransactionBase64 = trade.routerData;
655 + final requestId = trade.routerValue;
656 + if (swapTransactionBase64?.isNotEmpty == true &&
657 + requestId?.isNotEmpty == true &&
658 + solana != null) {
659 + try {
660 + final actualFee = trade.fee ?? 0.0005;
661 + // Fallback to estimate if not available
662 + final fee = actualFee > 0 ? actualFee : 0.0005;
663 +
664 + final amount = double.tryParse(trade.amount) ?? 0.0;
665 +
666 + pendingTransaction = await solana!.signAndPrepareJupiterSwapTransaction(
667 + wallet,
668 + swapTransactionBase64!,
669 + requestId!,
670 + trade.payoutAddress ?? '',
671 + amount,
672 + fee,
673 + );
674 +
675 + state = ExecutedSuccessfullyState();
676 + return pendingTransaction;
677 + } catch (e, s) {
678 + printV('Jupiter swap error: $e\n$s');
679 + throw Exception('Failed to process Jupiter swap: $e');
680 + }
681 + }
682 + }
683 +
684 // Regular flow
685
686 pendingTransaction = await wallet.createTransaction(_credentials(provider));
@@ -784,8 +816,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
816 if (walletType == WalletType.solana) {
817 Future.delayed(Duration(seconds: 1), () async {
818 try {
787 - // Updates tx history with the exact mints involved in transaction
788 - // Also updates balances for the tokens involved in the transaction
819 await solana!.pollForTransaction(
820 wallet,
821 pendingTransaction!.id,
@@ -793,9 +823,62 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
823 maxRetries: 5,
824 );
825 } catch (e) {
796 - printV('Failed to update transactions after send: $e');
826 + printV('Failed to poll for transaction: $e');
827 }
828 });
829 +
830 + // Update balances for currencies involved in swap
831 + if (_currentTrade != null) {
832 + Future.delayed(Duration(seconds: 2), () async {
833 + try {
834 + final tokenMints = <String>[];
835 +
836 + // Extract from currency mint (skip native SOL)
837 + if (_currentTrade!.from != null && _currentTrade!.from != CryptoCurrency.sol) {
838 + try {
839 + final fromMint = solana!.getTokenAddress(_currentTrade!.from!);
840 + tokenMints.add(fromMint);
841 + } catch (e) {
842 + printV('Error getting from currency mint: $e');
843 + }
844 + }
845 +
846 + // Extract to currency mint (skip native SOL)
847 + if (_currentTrade!.to != null && _currentTrade!.to != CryptoCurrency.sol) {
848 + try {
849 + final toMint = solana!.getTokenAddress(_currentTrade!.to!);
850 + tokenMints.add(toMint);
851 + } catch (e) {
852 + printV('Error getting to currency mint: $e');
853 + }
854 + }
855 +
856 + if (tokenMints.isNotEmpty) {
857 + solana!.updateTokenBalances(
858 + wallet,
859 + tokenMints: tokenMints,
860 + );
861 +
862 + // Retry after a bit more time to ensure balance is updated
863 + Future.delayed(Duration(seconds: 2), () async {
864 + try {
865 + await solana!.updateTokenBalances(
866 + wallet,
867 + tokenMints: tokenMints,
868 + );
869 + } catch (e) {
870 + printV('Error retrying balance update: $e');
871 + }
872 + });
873 + }
874 + } catch (e) {
875 + printV('Failed to update balances after send: $e');
876 + } finally {
877 + _currentTrade = null;
878 + _currentProvider = null;
879 + }
880 + });
881 + }
882 }
883
884 // Immediate transaction update for EVM chains, Tron, and Nano
@@ -835,22 +918,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
918
919 _currentTrade!.txId = signature;
920
838 - if (!isSuccess) {
839 - _currentTrade!.stateRaw = TradeState.failed.raw;
840 - if (_currentTrade!.isInBox) {
841 - await _currentTrade!.save();
842 - }
843 - }
844 -
845 - if (isSuccess) {
846 - _currentTrade!.stateRaw = TradeState.completed.raw;
847 -
848 - if (_currentTrade!.isInBox) {
849 - await _currentTrade!.save();
850 - }
921 + _currentTrade!.stateRaw = isSuccess ? TradeState.completed.raw : TradeState.failed.raw;
922
852 - _currentTrade = null;
853 - _currentProvider = null;
923 + if (_currentTrade!.isInBox) {
924 + await _currentTrade!.save();
925 }
926 }
927
lib/view_model/transaction_details_view_model.dart
+1 -1
@@ -555,7 +555,7 @@ abstract class TransactionDetailsViewModelBase with Store {
555 final _items = [
556 StandartListItem(
557 title: S.current.transaction_details_transaction_id,
558 - value: tx.txHash,
558 + value: tx.txHash.replaceAll(RegExp(r'_(incoming|outgoing)$'), ''),
559 key: ValueKey('standard_list_item_transaction_details_id_key'),
560 ),
561 StandartListItem(