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
This commit is contained in:
David Adegoke
2026-01-20 00:15:22 +01:00
committed by GitHub
parent cd2f87a76e
commit 696052f515
5 changed files with 105 additions and 27 deletions
+7 -3
View File
@@ -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();
}
@@ -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';
@@ -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(
+89 -18
View File
@@ -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();
}
}
_currentTrade!.stateRaw = isSuccess ? TradeState.completed.raw : TradeState.failed.raw;
if (isSuccess) {
_currentTrade!.stateRaw = TradeState.completed.raw;
if (_currentTrade!.isInBox) {
await _currentTrade!.save();
}
_currentTrade = null;
_currentProvider = null;
if (_currentTrade!.isInBox) {
await _currentTrade!.save();
}
}
@@ -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(