fix: solana wallet bugs and security issues (#3484)

* fix android CI

* fix: duplicate outgoing tx for jup swaps and stuck pending state

* fix solana security risk by handling duplicate token symbols in wallet transactions

* feat: implement additional cost handling for pending transactions in Solana wallet.

* fix: Items on security audit list for solana wallet

* refactor: streamline fee payer index handling and improve error logging

* fix: verifySignature for solana and handle wrong mint on default token

* fix: token decimals defaulting to zero and breaking amount parsing in solana

* fix: use token mask in tx history

* refactor: apply lint to modified code

* fix: merge conflicts

* fix: use Money and mint decimals when parsing sol swaps

* test: add unit tests for SPL token amount handling and parsing

* test: add more tests

* fix: update decimal handling for fetched token and remove unused fields
This commit is contained in:
David Adegoke
2026-08-24 23:21:29 +03:00
committed by GitHub
parent f97e09cfc5
commit 703cc22b33
52 changed files with 687 additions and 373 deletions
@@ -631,6 +631,7 @@ class CakePayBuyCardPage extends BasePage {
amountValue: _sendViewModel.amountParsingProxy
.asDisplayStringWithSymbol(_sendViewModel.pendingTransaction!.amount),
quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
explanation: _sendViewModel.pendingTransactionAdditionalCostNotice,
fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(bottomSheetContext).send_fee,
feeValue: _sendViewModel.amountParsingProxy
@@ -138,6 +138,10 @@ class JupiterExchangeProvider extends ExchangeProvider {
if (!_isSolanaCurrency(from) || !_isSolanaCurrency(to)) {
return 0.0;
}
if (amount <= 0) {
return 0.0;
}
final inputMint = _getTokenMint(from);
final outputMint = _getTokenMint(to);
@@ -100,9 +100,9 @@ class HistoryTile extends StatelessWidget {
children: [
Opacity(
opacity: pending ? 0.5 : 1,
child: CakeImageWidget(
child: TokenImageWidget(
imageUrl: asset?.iconPath ?? "",
width: 34,
size: 34,
),
),
Align(
@@ -201,6 +201,7 @@ class SendTransactionDetails extends StatelessWidget {
Widget _buildMainContent(BuildContext context) {
return Observer(builder: (context) {
final transaction = sendViewModel.pendingTransaction;
final additionalCostNotice = sendViewModel.pendingTransactionAdditionalCostNotice;
final currencySymbol =
sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency);
@@ -398,6 +399,26 @@ class SendTransactionDetails extends StatelessWidget {
),
),
),
if (additionalCostNotice != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Text(
additionalCostNotice,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
if (sendViewModel.isElectrumWallet) ...[
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
+8 -4
View File
@@ -130,11 +130,11 @@ class CWSolana extends Solana {
final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol);
if (token == null) {
throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}');
if (token != null) {
return token;
}
return token;
return transaction.amount.currency as CryptoCurrency;
}
@override
@@ -276,6 +276,8 @@ class CWSolana extends Solana {
'Jupiter swap returned unknown status: $status. Error: $errorMessage. Code: $errorCode',
);
}
} on JupiterSwapFailedException {
rethrow;
} catch (e) {
throw Exception('Failed to execute Jupiter swap: $e');
}
@@ -420,7 +422,9 @@ class CWSolana extends Solana {
if (discoveredMints.isNotEmpty) {
await wallet.updateSPLTokenTransactions(specificMints: discoveredMints);
}
} catch (_) {}
} catch (e) {
printV("Error discovering wallet tokens: ${e.toString()}");
}
}
@override
@@ -328,6 +328,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
amount: S.of(bottomSheetContext).send_amount,
amountValue: sendVM.amountParsingProxy
.getDisplayCryptoAmount(amountValue, sendVM.selectedCryptoCurrency),
explanation: sendVM.pendingTransactionAdditionalCostNotice,
fiatAmountValue: fiatAmountValue,
fee: isEVMCompatibleChain(sendVM.walletType)
? S.of(bottomSheetContext).send_estimated_fee
+20 -4
View File
@@ -336,6 +336,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@observable
PendingTransaction? pendingTransaction;
String? get pendingTransactionAdditionalCostNotice {
final additionalCost = pendingTransaction?.additionalCost;
if (additionalCost == null) {
return null;
}
return S.current.recipient_account_creation_fee(
_appStore.amountParsingProxy.asDisplayStringWithSymbol(additionalCost),
);
}
@computed
String get balance {
if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.mweb) {
@@ -1179,11 +1191,11 @@ 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);
final failedSignature = e is JupiterSwapFailedException ? e.signature : "";
await _updateSolanaTrade(signature: failedSignature, isSuccess: false);
}
}
@@ -1509,6 +1521,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return S.current.solana_no_associated_token_account_exception;
}
if (error is AmbiguousTokenSymbolException) {
return S.current.ambiguous_token_symbol_exception(error.symbol);
}
if (errorMessage.contains('found no record of a prior credit')) {
return S.current.insufficient_funds_for_tx;
}