feat: Integrate Jupiter DEX (#2761)

* 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

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2026-01-16 23:28:36 +01:00
committed by GitHub
co-authored by Omar Hatem
parent 368777514e
commit cf795bcc48
16 changed files with 1509 additions and 67 deletions
@@ -8,6 +8,7 @@ import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
@@ -86,6 +87,9 @@ abstract class ExchangeTradeViewModelBase with Store {
case ExchangeProviderDescription.swapsXyz:
_provider = SwapsXyzExchangeProvider();
break;
case ExchangeProviderDescription.jupiter:
_provider = JupiterExchangeProvider();
break;
case ExchangeProviderDescription.nearIntents:
_provider = NearIntentsExchangeProvider();
break;
@@ -118,6 +122,19 @@ abstract class ExchangeTradeViewModelBase with Store {
isEVMCompatibleChain(wallet.type) &&
wallet.currency != trade.from;
/// Providers that should hide the "send from external" button
static const List<Type> _providersThatHideExternalSend = [
JupiterExchangeProvider,
];
/// Returns true if the current provider should hide the external send button
bool get shouldHideExternalSendButton {
if (_provider == null) return false;
return _providersThatHideExternalSend.any(
(providerType) => _provider.runtimeType == providerType,
);
}
String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty
? '\n\n' + S.current.exchange_extra_info
: '';
@@ -21,6 +21,7 @@ import 'package:cake_wallet/exchange/exchange_trade_state.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/limits_state.dart';
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
@@ -229,6 +230,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
LetsExchangeExchangeProvider(),
StealthExExchangeProvider(),
XOSwapExchangeProvider(),
JupiterExchangeProvider(),
// SwapsXyzExchangeProvider(),
NearIntentsExchangeProvider(),
TrocadorExchangeProvider(
+98 -5
View File
@@ -21,15 +21,17 @@ import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@@ -136,6 +138,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
PendingTransaction? _pendingApprovalTx;
bool _isSwapsXYZCallDataTx = false;
// Store trade and provider references for post-commit updates (e.g., Jupiter trade ID update)
Trade? _currentTrade;
ExchangeProvider? _currentProvider;
@observable
ExecutionState state;
@@ -516,6 +522,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@action
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider, Trade? trade}) async {
_currentTrade = trade;
_currentProvider = provider;
try {
if (!(state is IsExecutingState)) state = IsExecutingState();
@@ -608,6 +617,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));
@@ -738,12 +779,33 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
state = TransactionCommitted();
// Immediate transaction update for EVM chains, Solana, Tron, and Nano
if (isEVMWallet ||
[WalletType.solana, WalletType.tron, WalletType.nano].contains(walletType)) {
await _updateSolanaTrade(signature: pendingTransaction!.id, isSuccess: true);
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,
initialDelay: const Duration(seconds: 1),
maxRetries: 5,
);
} catch (e) {
printV('Failed to update transactions after send: $e');
}
});
}
// Immediate transaction update for EVM chains, Tron, and Nano
if (isEVMWallet || [WalletType.tron, WalletType.nano].contains(walletType)) {
Future.delayed(Duration(seconds: 4), () async {
try {
await wallet.updateTransactionsHistory();
await Future.wait([
wallet.updateTransactionsHistory(),
wallet.updateBalance() as Future<void>,
]);
} catch (e) {
printV('Failed to update transactions after send: $e');
}
@@ -757,7 +819,38 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
DateTime.now().add(Duration(minutes: 1)).toIso8601String());
} catch (e) {
if (e is JupiterSwapFailedException) {
await _updateSolanaTrade(signature: e.signature, isSuccess: false);
}
state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
await _updateSolanaTrade(signature: '', isSuccess: false);
}
}
/// Update Jupiter trade with relevant details after transaction is committed
Future<void> _updateSolanaTrade({required String signature, required bool isSuccess}) async {
if (_currentTrade == null ||
_currentProvider?.title != 'Jupiter' ||
walletType != WalletType.solana) return;
_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 = null;
_currentProvider = null;
}
}
@@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
@@ -82,6 +83,9 @@ abstract class TradeDetailsViewModelBase with Store {
case ExchangeProviderDescription.swapsXyz:
_provider = SwapsXyzExchangeProvider();
break;
case ExchangeProviderDescription.jupiter:
_provider = JupiterExchangeProvider();
break;
case ExchangeProviderDescription.nearIntents:
_provider = NearIntentsExchangeProvider();
break;
@@ -119,6 +123,8 @@ abstract class TradeDetailsViewModelBase with Store {
return 'https://scan.chainflip.io/channels/${trade.id}';
case ExchangeProviderDescription.xoSwap:
return 'https://orders.xoswap.com/${trade.id}';
case ExchangeProviderDescription.jupiter:
return 'https://solscan.io/tx/${trade.txId}';
case ExchangeProviderDescription.nearIntents:
return 'https://explorer.near-intents.org/transactions/${trade.id}';
}