mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +02:00
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:
co-authored by
Omar Hatem
parent
368777514e
commit
cf795bcc48
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
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/simpleswap_exchange_provider.dart';
|
||||
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
|
||||
@@ -66,6 +67,8 @@ class TradeMonitor {
|
||||
return XOSwapExchangeProvider();
|
||||
case ExchangeProviderDescription.swapsXyz:
|
||||
return SwapsXyzExchangeProvider();
|
||||
case ExchangeProviderDescription.jupiter:
|
||||
return JupiterExchangeProvider();
|
||||
case ExchangeProviderDescription.nearIntents:
|
||||
return NearIntentsExchangeProvider();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
|
||||
ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/images/swaps_xyz.svg');
|
||||
static const nearIntents =
|
||||
ExchangeProviderDescription(title: 'Near Intents', raw: 15, image: 'assets/images/near.png');
|
||||
static const jupiter =
|
||||
ExchangeProviderDescription(title: 'Jupiter', raw: 16, image: 'assets/images/jupiter.png');
|
||||
|
||||
static ExchangeProviderDescription deserialize({required int raw}) {
|
||||
switch (raw) {
|
||||
@@ -75,6 +77,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
|
||||
return swapsXyz;
|
||||
case 15:
|
||||
return nearIntents;
|
||||
case 16:
|
||||
return jupiter;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
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';
|
||||
import 'package:cake_wallet/exchange/trade.dart';
|
||||
import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:cake_wallet/exchange/trade_request.dart';
|
||||
import 'package:cake_wallet/exchange/trade_state.dart';
|
||||
import 'package:cake_wallet/solana/solana.dart';
|
||||
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
|
||||
import 'package:cw_core/amount_converter.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
class JupiterExchangeProvider extends ExchangeProvider {
|
||||
JupiterExchangeProvider() : super(pairList: _getSupportedPairs());
|
||||
|
||||
// Jupiter only supports Solana tokens
|
||||
static const List<CryptoCurrency> _notSupported = [];
|
||||
|
||||
static List<ExchangePair> _getSupportedPairs() {
|
||||
// Only support Solana and Solana tokens
|
||||
final solanaCurrencies = CryptoCurrency.all
|
||||
.where((c) => c.tag == 'SOL' || c == CryptoCurrency.sol)
|
||||
.where((c) => !_notSupported.contains(c))
|
||||
.toList();
|
||||
|
||||
final pairs = <ExchangePair>[];
|
||||
for (final from in solanaCurrencies) {
|
||||
for (final to in solanaCurrencies) {
|
||||
if (from != to) {
|
||||
pairs.add(ExchangePair(from: from, to: to, reverse: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
static const _baseUrl = 'api.jup.ag';
|
||||
static const _orderPath = '/ultra/v1/order';
|
||||
static const _executePath = '/ultra/v1/execute';
|
||||
|
||||
// Wrapped SOL address (native SOL)
|
||||
static const _nativeSolMint = 'So11111111111111111111111111111111111111112';
|
||||
|
||||
@override
|
||||
String get title => 'Jupiter';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => false; // Jupiter doesn't support fixed rate
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.jupiter;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
String _getTokenMint(CryptoCurrency currency) {
|
||||
// Handle native SOL
|
||||
if (currency == CryptoCurrency.sol) return _nativeSolMint;
|
||||
|
||||
// Check if currency tag is SOL (indicating it's a Solana token)
|
||||
if (currency.tag != 'SOL') {
|
||||
throw Exception('Unsupported currency: ${currency.title} (not a Solana token)');
|
||||
}
|
||||
|
||||
// Use solana proxy to get token address
|
||||
// The proxy will handle both SPLToken instances and CryptoCurrency
|
||||
// by searching through default tokens
|
||||
if (solana != null) {
|
||||
try {
|
||||
return solana!.getTokenAddress(currency);
|
||||
} catch (e) {
|
||||
printV('Error getting token address: $e');
|
||||
throw Exception('Unsupported currency: ${currency.title} (mint address not found: $e)');
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Unsupported currency: ${currency.title} (Solana proxy not available)');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
}) async {
|
||||
try {
|
||||
// The Ultra Swap API doesn't have a dedicated limits endpoint
|
||||
// The /order endpoint validates amounts and returns error codes:
|
||||
// - errorCode 1: Insufficient funds
|
||||
// - errorCode 2: Top up SOL for gas
|
||||
// - errorCode 3: Minimum amount for gasless
|
||||
return Limits(min: null, max: null);
|
||||
} catch (e) {
|
||||
printV('fetchLimits error: $e');
|
||||
throw Exception('Error fetching limits: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> _getHeaders() {
|
||||
final headers = <String, String>{};
|
||||
final apiKey = secrets.jupiterApiKey;
|
||||
if (apiKey.isNotEmpty) {
|
||||
headers['x-api-key'] = apiKey;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
Map<String, String>? _getReferralFeeConfig() {
|
||||
try {
|
||||
final referralFeeBpsStr = secrets.jupiterReferralFeeBps;
|
||||
final referralFeeBps = int.tryParse(referralFeeBpsStr) ?? 0;
|
||||
|
||||
final referralAccount = secrets.jupiterReferralAccount;
|
||||
|
||||
// Only enable if both are configured and valid
|
||||
if (referralFeeBps <= 0 || referralFeeBps > 10000 || referralAccount.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
'referralFee': referralFeeBps.toString(),
|
||||
'referralAccount': referralAccount,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> fetchRate({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode,
|
||||
required bool isReceiveAmount,
|
||||
}) async {
|
||||
try {
|
||||
final inputMint = _getTokenMint(from);
|
||||
final outputMint = _getTokenMint(to);
|
||||
|
||||
final amountInBaseUnits = AmountConverter.toBaseUnits(amount.toString(), from.decimals);
|
||||
|
||||
final params = {
|
||||
'inputMint': inputMint,
|
||||
'outputMint': outputMint,
|
||||
'amount': amountInBaseUnits,
|
||||
// Note: taker is optional for quote-only requests
|
||||
};
|
||||
|
||||
final uri = Uri.https(_baseUrl, _orderPath, params);
|
||||
final headers = _getHeaders();
|
||||
|
||||
final response = await ProxyWrapper().get(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: Exception('Failed to fetch quote: ${response.statusCode}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
final orderData = json.decode(response.body) as Map<String, dynamic>;
|
||||
final outAmount = BigInt.parse(orderData['outAmount'] as String);
|
||||
|
||||
final outputAmount = AmountConverter.fromBaseUnits(outAmount.toString(), to.decimals);
|
||||
|
||||
final rate = double.parse(outputAmount) / amount;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
responseData: {
|
||||
'rate': rate,
|
||||
'outputAmount': outputAmount,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
printV('fetchRate error: $e');
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
try {
|
||||
final inputMint = _getTokenMint(request.fromCurrency);
|
||||
final outputMint = _getTokenMint(request.toCurrency);
|
||||
|
||||
final amountInBaseUnits =
|
||||
AmountConverter.toBaseUnits(request.fromAmount, request.fromCurrency.decimals);
|
||||
|
||||
final isInternalTransfer = request.refundAddress == request.toAddress;
|
||||
|
||||
final orderParams = <String, String>{
|
||||
'inputMint': inputMint,
|
||||
'outputMint': outputMint,
|
||||
'amount': amountInBaseUnits,
|
||||
'taker': request.refundAddress,
|
||||
if (!isInternalTransfer) 'receiver': request.toAddress,
|
||||
};
|
||||
|
||||
final referralFeeConfig = _getReferralFeeConfig();
|
||||
if (referralFeeConfig != null) {
|
||||
orderParams['referralFee'] = referralFeeConfig['referralFee']!;
|
||||
orderParams['referralAccount'] = referralFeeConfig['referralAccount']!;
|
||||
}
|
||||
|
||||
final orderUri = Uri.https(_baseUrl, _orderPath, orderParams);
|
||||
final headers = _getHeaders();
|
||||
|
||||
final orderResponse = await ProxyWrapper().get(clearnetUri: orderUri, headers: headers);
|
||||
|
||||
if (orderResponse.statusCode != 200) {
|
||||
final errorBody = orderResponse.body;
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: Exception('Failed to get order: ${orderResponse.statusCode} $errorBody'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
},
|
||||
);
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
|
||||
final orderData = json.decode(orderResponse.body) as Map<String, dynamic>;
|
||||
|
||||
// Check for errors in response
|
||||
if (orderData.containsKey('errorCode') || orderData.containsKey('errorMessage')) {
|
||||
final errorCode = orderData['errorCode'];
|
||||
final errorMessage = orderData['errorMessage'] ?? 'Unknown error';
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: Exception('Order error: $errorCode - $errorMessage'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
},
|
||||
);
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
|
||||
// Extract response data
|
||||
final transaction = orderData['transaction'] as String?;
|
||||
final requestId = orderData['requestId'] as String?;
|
||||
final outAmount = orderData['outAmount'] as String? ?? '0.0';
|
||||
|
||||
// Extract network fees from order response (in lamports)
|
||||
final signatureFeeLamports = (orderData['signatureFeeLamports'] as num?)?.toInt() ?? 0;
|
||||
|
||||
final prioritizationFeeLamports =
|
||||
(orderData['prioritizationFeeLamports'] as num?)?.toInt() ?? 0;
|
||||
|
||||
final rentFeeLamports = (orderData['rentFeeLamports'] as num?)?.toInt() ?? 0;
|
||||
|
||||
final integratorFeeLamports = (orderData['integratorFeeLamports'] as num?)?.toInt() ?? 0;
|
||||
|
||||
final networkFeeLamports = signatureFeeLamports + prioritizationFeeLamports + rentFeeLamports;
|
||||
final networkFeeInSol = networkFeeLamports / 1000000000.0;
|
||||
|
||||
final integratorFeeInSol = integratorFeeLamports / 1000000000.0;
|
||||
|
||||
final totalFeeInSol = networkFeeInSol + integratorFeeInSol;
|
||||
|
||||
if (transaction == null || transaction.isEmpty) {
|
||||
throw Exception('No transaction returned from Jupiter order endpoint');
|
||||
}
|
||||
|
||||
if (requestId == null || requestId.isEmpty) {
|
||||
throw Exception('No requestId returned from Jupiter order endpoint');
|
||||
}
|
||||
|
||||
final receiveAmount = AmountConverter.fromBaseUnits(outAmount, request.toCurrency.decimals);
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
},
|
||||
responseData: {
|
||||
'tradeId': requestId,
|
||||
'receiveAmount': receiveAmount,
|
||||
'hasTransaction': transaction.isNotEmpty,
|
||||
'requestId': requestId,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: requestId,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
provider: description,
|
||||
inputAddress: request.toAddress,
|
||||
refundAddress: request.refundAddress,
|
||||
state: TradeState.created,
|
||||
createdAt: DateTime.now(),
|
||||
amount: request.fromAmount,
|
||||
receiveAmount: receiveAmount,
|
||||
payoutAddress: request.toAddress,
|
||||
isSendAll: isSendAll,
|
||||
userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? 'SOL'}',
|
||||
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? 'SOL'}',
|
||||
routerData: transaction,
|
||||
routerValue: requestId,
|
||||
fee: totalFeeInSol,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
},
|
||||
);
|
||||
printV('createTrade error: $e');
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a signed Jupiter swap transaction via Jupiter's /execute endpoint
|
||||
Future<Map<String, dynamic>> executeSwap({
|
||||
required String signedTransaction,
|
||||
required String requestId,
|
||||
}) async {
|
||||
try {
|
||||
final executeUri = Uri.https(_baseUrl, _executePath);
|
||||
final headers = _getHeaders();
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
final body = json.encode({
|
||||
'signedTransaction': signedTransaction,
|
||||
'requestId': requestId,
|
||||
});
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: executeUri,
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final errorBody = response.body;
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'executeSwap',
|
||||
error: Exception('Failed to execute swap: ${response.statusCode} $errorBody'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'requestId': requestId,
|
||||
'hasSignedTransaction': signedTransaction.isNotEmpty,
|
||||
},
|
||||
);
|
||||
throw Exception('Failed to execute swap: ${response.statusCode} $errorBody');
|
||||
}
|
||||
|
||||
final executeData = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'executeSwap',
|
||||
requestData: {
|
||||
'requestId': requestId,
|
||||
'hasSignedTransaction': signedTransaction.isNotEmpty,
|
||||
},
|
||||
responseData: executeData,
|
||||
);
|
||||
|
||||
return executeData;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'executeSwap',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'requestId': requestId,
|
||||
'hasSignedTransaction': signedTransaction.isNotEmpty,
|
||||
},
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
// Jupiter Ultra Swap API doesn't track trades by our trade ID
|
||||
//
|
||||
// Status tracking options:
|
||||
// 1. Use /execute endpoint with requestId + signedTransaction (requires storing signed tx)
|
||||
// 2. Check on-chain via transaction signature (txId) after transaction is sent
|
||||
//
|
||||
// Current implementation: We track status on-chain via transaction signature
|
||||
// The txId field in Trade is set after the transaction is sent and can be
|
||||
// used to check transaction status via Solana RPC.
|
||||
//
|
||||
// Note: To use /execute endpoint for status polling, we would need to:
|
||||
// - Store the signed transaction (not currently stored)
|
||||
// - Use requestId from routerValue
|
||||
// - Poll /ultra/v1/execute with both signedTransaction and requestId
|
||||
//
|
||||
// For now, throw exception to indicate status must be checked on-chain
|
||||
throw Exception(
|
||||
'Jupiter trade status must be checked on-chain using transaction signature (txId). '
|
||||
'After transaction is sent, txId will contain the signature for status checking.');
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ class Trade extends HiveObject {
|
||||
this.providerName,
|
||||
this.fromWalletAddress,
|
||||
this.memo,
|
||||
this.fee,
|
||||
this.txId,
|
||||
this.isRefund,
|
||||
this.isSendAll,
|
||||
@@ -175,6 +176,9 @@ class Trade extends HiveObject {
|
||||
@HiveField(34)
|
||||
int? chainId;
|
||||
|
||||
@HiveField(35)
|
||||
double? fee;
|
||||
|
||||
CryptoCurrency? get userCurrencyFrom {
|
||||
if (userCurrencyFromRaw == null || userCurrencyFromRaw!.isEmpty) {
|
||||
return null;
|
||||
@@ -257,6 +261,7 @@ class Trade extends HiveObject {
|
||||
'router': router,
|
||||
'extra_id': extraId,
|
||||
'chain_id': chainId,
|
||||
'fee': fee,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -310,6 +315,7 @@ class TradeAdapter extends TypeAdapter<Trade> {
|
||||
sourceTokenAmountRaw: fields[32] as String?,
|
||||
requiresTokenApproval: fields[33] as bool?,
|
||||
chainId: fields[34] as int?,
|
||||
fee: fields[35] as double?,
|
||||
)
|
||||
..providerRaw = fields[1] == null ? 0 : fields[1] as int
|
||||
..fromRaw = (fields[2] as int?) ?? -1
|
||||
@@ -390,7 +396,9 @@ class TradeAdapter extends TypeAdapter<Trade> {
|
||||
..writeByte(33)
|
||||
..write(obj.requiresTokenApproval)
|
||||
..writeByte(34)
|
||||
..write(obj.chainId);
|
||||
..write(obj.chainId)
|
||||
..writeByte(35)
|
||||
..write(obj.fee);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+188
-3
@@ -4,8 +4,7 @@ class CWSolana extends Solana {
|
||||
@override
|
||||
List<String> getSolanaWordList(String language) => SolanaMnemonics.englishWordlist;
|
||||
|
||||
WalletService createSolanaWalletService(bool isDirect) =>
|
||||
SolanaWalletService(isDirect);
|
||||
WalletService createSolanaWalletService(bool isDirect) => SolanaWalletService(isDirect);
|
||||
|
||||
@override
|
||||
WalletCredentials createSolanaNewWalletCredentials({
|
||||
@@ -135,7 +134,29 @@ class CWSolana extends Solana {
|
||||
}
|
||||
|
||||
@override
|
||||
String getTokenAddress(CryptoCurrency asset) => (asset as SPLToken).mintAddress;
|
||||
String getTokenAddress(CryptoCurrency asset) {
|
||||
// If it's already an SPLToken, use its mint address
|
||||
if (asset is SPLToken) return asset.mintAddress;
|
||||
|
||||
// If it's not an SPLToken but has SOL tag, try to find matching SPLToken
|
||||
if (asset.tag == 'SOL') {
|
||||
final symbol = asset.title.toUpperCase();
|
||||
|
||||
// Search through default tokens to find matching symbol
|
||||
final defaultTokens = DefaultSPLTokens().initialSPLTokens;
|
||||
try {
|
||||
final matchingToken = defaultTokens.firstWhere(
|
||||
(token) => token.symbol.toUpperCase() == symbol,
|
||||
);
|
||||
return matchingToken.mintAddress;
|
||||
} catch (_) {
|
||||
// Token not found in default tokens
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback - try to cast (will throw if not SPLToken)
|
||||
return (asset as SPLToken).mintAddress;
|
||||
}
|
||||
|
||||
@override
|
||||
List<int>? getValidationLength(CryptoCurrency type) {
|
||||
@@ -161,4 +182,168 @@ class CWSolana extends Solana {
|
||||
final solanaWallet = wallet as SolanaWallet;
|
||||
return solanaWallet.splTokenCurrencies.any((element) => element.mintAddress == contractAddress);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> signAndPrepareJupiterSwapTransaction(
|
||||
WalletBase wallet,
|
||||
String base64Transaction,
|
||||
String requestId,
|
||||
String destinationAddress,
|
||||
double amount,
|
||||
double fee,
|
||||
) async {
|
||||
final solanaWallet = wallet as SolanaWallet;
|
||||
final privateKey = solanaWallet.solanaPrivateKey;
|
||||
final solanaProvider = solanaWallet.solanaProvider;
|
||||
|
||||
if (solanaProvider == null) {
|
||||
throw Exception('Solana provider not available');
|
||||
}
|
||||
|
||||
final unsignedTransactionBytes = base64.decode(base64Transaction);
|
||||
final unsignedTransaction = SolanaTransaction.deserialize(unsignedTransactionBytes);
|
||||
|
||||
final signedMessage = privateKey.sign(unsignedTransaction.serializeMessage());
|
||||
unsignedTransaction.addSignature(privateKey.publicKey().toAddress(), signedMessage);
|
||||
|
||||
final signedTransactionBytes = unsignedTransaction.serialize();
|
||||
final signedTransactionBase64 = base64.encode(Uint8List.fromList(signedTransactionBytes));
|
||||
|
||||
Future<String> sendTx() async {
|
||||
try {
|
||||
if (signedTransactionBase64.isEmpty) {
|
||||
throw Exception('Invalid transaction: transaction is empty');
|
||||
}
|
||||
|
||||
if (requestId.isEmpty) {
|
||||
throw Exception('Invalid requestId: requestId is empty');
|
||||
}
|
||||
|
||||
final jupiterProvider = JupiterExchangeProvider();
|
||||
|
||||
final executeResponse = await jupiterProvider.executeSwap(
|
||||
signedTransaction: signedTransactionBase64,
|
||||
requestId: requestId,
|
||||
);
|
||||
|
||||
final status = executeResponse['status'] as String?;
|
||||
final signature = executeResponse['signature'] as String?;
|
||||
final errorCode = executeResponse['code'] as num?;
|
||||
final errorMessage = executeResponse['error'] as String? ?? 'Unknown error';
|
||||
|
||||
// Handle different status cases
|
||||
switch (status) {
|
||||
case 'Success':
|
||||
if (signature == null ||
|
||||
signature.isEmpty ||
|
||||
signature == '1111111111111111111111111111111111111111111111111111111111111111') {
|
||||
throw Exception(
|
||||
'Invalid transaction signature received from Jupiter. '
|
||||
'Status: $status',
|
||||
);
|
||||
}
|
||||
return signature;
|
||||
case 'Failed':
|
||||
String userFriendlyError = _getJupiterErrorMessage(errorCode, errorMessage);
|
||||
// Even when failed, Jupiter may return a signature for solscan
|
||||
if (signature != null && signature.isNotEmpty) {
|
||||
throw JupiterSwapFailedException(
|
||||
message: userFriendlyError,
|
||||
signature: signature,
|
||||
errorCode: errorCode,
|
||||
errorMessage: errorMessage,
|
||||
);
|
||||
} else {
|
||||
throw Exception(userFriendlyError);
|
||||
}
|
||||
case 'Pending':
|
||||
case 'Processing':
|
||||
throw Exception(
|
||||
'Jupiter swap is still processing. Please wait and try checking the transaction status.',
|
||||
);
|
||||
default:
|
||||
throw Exception(
|
||||
'Jupiter swap returned unknown status: $status. Error: $errorMessage. Code: $errorCode',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to execute Jupiter swap: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return PendingSolanaTransaction(
|
||||
amount: amount,
|
||||
serializedTransaction: signedTransactionBase64,
|
||||
destinationAddress: destinationAddress,
|
||||
sendTransaction: sendTx,
|
||||
fee: fee,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get user-friendly error message based on Jupiter error code
|
||||
String _getJupiterErrorMessage(num? errorCode, String errorMessage) {
|
||||
if (errorCode == null) {
|
||||
return 'Jupiter swap failed: $errorMessage';
|
||||
}
|
||||
|
||||
switch (errorCode.toInt()) {
|
||||
case -2000:
|
||||
return 'Transaction failed to land on the network. Please try again.';
|
||||
case -2001:
|
||||
return 'Unknown error occurred. Please try again.';
|
||||
case -2002:
|
||||
return 'Invalid transaction. Please try creating a new swap.';
|
||||
case -2003:
|
||||
return 'Quote expired. The swap quote is no longer valid. Please create a new swap.';
|
||||
case -2004:
|
||||
return 'Swap was rejected. This may be due to:\n'
|
||||
'- Insufficient funds for the swap or fees\n'
|
||||
'- Slippage tolerance exceeded (price moved too much)\n'
|
||||
'- Network congestion\n'
|
||||
'Please check your balance and try again with a new quote.';
|
||||
case -2005:
|
||||
return 'Internal error occurred. Please try again.';
|
||||
default:
|
||||
// Check for common program errors
|
||||
if (errorMessage.contains('SlippageToleranceExceeded') ||
|
||||
errorMessage.contains('slippage')) {
|
||||
return 'Slippage tolerance exceeded. The price moved too much during the swap. '
|
||||
'Please try again with a new quote.';
|
||||
}
|
||||
|
||||
if (errorMessage.contains('InsufficientFunds') || errorMessage.contains('insufficient')) {
|
||||
return 'Insufficient funds. Please ensure you have enough SOL for the swap and fees.';
|
||||
}
|
||||
|
||||
if (errorMessage.contains('Blockhash') || errorMessage.contains('expired')) {
|
||||
return 'Transaction expired. Please create a new swap.';
|
||||
}
|
||||
|
||||
return 'Jupiter swap failed (code: $errorCode): $errorMessage. Please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pollForTransaction(
|
||||
WalletBase wallet,
|
||||
String signature, {
|
||||
Duration initialDelay = const Duration(seconds: 1),
|
||||
int maxRetries = 5,
|
||||
}) async {
|
||||
final solanaWallet = wallet as SolanaWallet;
|
||||
await solanaWallet.pollForTransaction(
|
||||
signature: signature,
|
||||
initialDelay: initialDelay,
|
||||
maxRetries: maxRetries,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateTokenBalances(
|
||||
WalletBase wallet, {
|
||||
List<String>? tokenMints,
|
||||
}) async {
|
||||
final solanaWallet = wallet as SolanaWallet;
|
||||
await solanaWallet.updateTokenBalance(tokenMints: tokenMints);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class SyncIndicatorIcon extends StatelessWidget {
|
||||
static const String finished = 'finished';
|
||||
static const String success = 'success';
|
||||
static const String complete = 'complete';
|
||||
static const String completed = 'completed';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -39,6 +40,7 @@ class SyncIndicatorIcon extends StatelessWidget {
|
||||
case finished:
|
||||
case success:
|
||||
case complete:
|
||||
case completed:
|
||||
indicatorColor = CustomThemeColors.syncGreen;
|
||||
break;
|
||||
case waiting:
|
||||
|
||||
@@ -163,20 +163,21 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
bottomSection: Column(
|
||||
children: [
|
||||
PrimaryButton(
|
||||
key: ValueKey('exchange_trade_page_send_from_external_button_key'),
|
||||
text: S.current.send_from_external_wallet,
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage);
|
||||
},
|
||||
color: widget.exchangeTradeViewModel.isSendable
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
textColor: widget.exchangeTradeViewModel.isSendable
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: widget.exchangeTradeViewModel.isSwapsXyzSendingEVMTokenSwap,
|
||||
),
|
||||
if (!widget.exchangeTradeViewModel.shouldHideExternalSendButton)
|
||||
PrimaryButton(
|
||||
key: ValueKey('exchange_trade_page_send_from_external_button_key'),
|
||||
text: S.current.send_from_external_wallet,
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage);
|
||||
},
|
||||
color: widget.exchangeTradeViewModel.isSendable
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
textColor: widget.exchangeTradeViewModel.isSendable
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: widget.exchangeTradeViewModel.isSwapsXyzSendingEVMTokenSwap,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
@@ -188,7 +189,8 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
!(sendingState is TransactionCommitted)),
|
||||
child: LoadingPrimaryButton(
|
||||
key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
|
||||
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty ||
|
||||
isDisabled: trade.inputAddress == null ||
|
||||
trade.inputAddress!.isEmpty ||
|
||||
sendingState is ExecutedSuccessfullyState,
|
||||
isLoading: sendingState is IsExecutingState,
|
||||
onPressed: _onPressedSendFromCakeWallet,
|
||||
@@ -213,8 +215,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: sendVM.walletType,
|
||||
hardwareWalletType:
|
||||
sendVM.wallet.walletInfo.hardwareWalletType!,
|
||||
hardwareWalletType: sendVM.wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (context, _) {
|
||||
sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet);
|
||||
Navigator.of(context).pop();
|
||||
@@ -308,7 +309,8 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
return ConfirmSendingBottomSheet(
|
||||
key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'),
|
||||
footerType: FooterType.slideActionButton,
|
||||
isSlideActionEnabled: widget.exchangeTradeViewModel.sendViewModel.isReadyForSend,
|
||||
isSlideActionEnabled:
|
||||
widget.exchangeTradeViewModel.sendViewModel.isReadyForSend,
|
||||
walletType: widget.exchangeTradeViewModel.sendViewModel.walletType,
|
||||
titleText: S.of(bottomSheetContext).confirm_transaction,
|
||||
titleIconPath:
|
||||
@@ -338,8 +340,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
},
|
||||
);
|
||||
|
||||
if (result == null) widget.exchangeTradeViewModel.sendViewModel.dismissTransaction();
|
||||
|
||||
if (result == null) widget.exchangeTradeViewModel.sendViewModel.dismissTransaction();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user