feat: Add token to recieve address QR and link to pay anything flow on Solana and Tron, also fix token not showing on evm wallets QRs when scanned from dashboard (#3320)
What changed, and why it matters
This commit changes how Cake Wallet builds and reads QR-code payment links for Solana and Tron tokens, and fixes a bug where EVM token QR codes did not include the token identifier. It also refactors how the app looks up a token from its contract/mint address. The changes are feature additions and a bug fix; there is no direct evidence in the diff of a security vulnerability, but any change to payment URI parsing and token lookup can affect whether a user sends funds to the wrong token or wrong address.
Treat as a normal feature/bug-fix review. Verify that scanned contract/mint addresses are validated against the wallet's known token list before being presented as the active send asset, and confirm the URI parser rejects malformed or unexpected query parameters. No emergency response is indicated by the diff alone.
Security signals we found
Payment URI parsing now accepts contract/mint address parameters from external QR codes/scanned links
Token selection is driven by a parsed contract address, which could influence which asset the user is prompted to send
Refactored token lookup reduces code duplication but does not add new validation
No explicit security claim or advisory is made in the commit message
Evidence from the diff
The patch extends SolanaURI and TronURI to optionally include a contractAddress/mintAddress query parameter (spl-token=… and token=…), updates PaymentRequest parsing to read those parameters, wires the parsed contract address into the send page so the wallet fetches the matching token, and refactors TokenUtilities.findTokenByAddress to use a unified token list per network. The EVM QR fix is implied by the existing Erc20Token branch in wallet_address_list_view_model.dart now being complemented by Tron and Solana branches. No input validation, sanitization, or trust-boundary changes are visible in the diff beyond existing null/empty checks.
Changed components
cw_core/lib/payment_uris.dartlib/utils/payment_request.dartlib/utils/token_utilities.dartlib/new-ui/pages/send_page.dartlib/view_model/wallet_address_list/wallet_address_list_view_model.dartInspect captured patch +64 / −31
diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart
index 6ad43a87..daf56af2 100644
--- a/cw_core/lib/payment_uris.dart
+++ b/cw_core/lib/payment_uris.dart
@@ -176,14 +176,23 @@ class PolygonURI extends PaymentURI {
}
class SolanaURI extends PaymentURI {
- SolanaURI({required super.amount, required super.address});
+ SolanaURI({required super.amount, required super.address, this.contractAddress});
+
+ final String? contractAddress;
@override
String toString() {
var base = 'solana:$address';
+ final params = <String>[];
if (amount.isNotEmpty) {
- base += '?amount=${amount.replaceAll(',', '.')}';
+ params.add('amount=${amount.replaceAll(',', '.')}');
+ }
+ if (contractAddress != null && contractAddress!.isNotEmpty) {
+ params.add('spl-token=$contractAddress');
+ }
+ if (params.isNotEmpty) {
+ base += '?${params.join('&')}';
}
return base;
@@ -191,14 +200,23 @@ class SolanaURI extends PaymentURI {
}
class TronURI extends PaymentURI {
- TronURI({required super.amount, required super.address});
+ TronURI({required super.amount, required super.address, this.contractAddress});
+
+ final String? contractAddress;
@override
String toString() {
var base = 'tron:$address';
+ final params = <String>[];
if (amount.isNotEmpty) {
- base += '?amount=${amount.replaceAll(',', '.')}';
+ params.add('amount=${amount.replaceAll(',', '.')}');
+ }
+ if (contractAddress != null && contractAddress!.isNotEmpty) {
+ params.add('token=$contractAddress');
+ }
+ if (params.isNotEmpty) {
+ base += '?${params.join('&')}';
}
return base;
diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart
index af09b68a..fec5e06f 100644
--- a/lib/new-ui/pages/send_page.dart
+++ b/lib/new-ui/pages/send_page.dart
@@ -228,6 +228,14 @@ class _NewSendPageState extends State<NewSendPage> {
_addressControllers[0].text = widget.initialPaymentRequest!.address;
_amountControllers[0].text = widget.initialPaymentRequest!.amount;
_memoControllers[0].text = widget.initialPaymentRequest!.note;
+ final contractAddress = widget.initialPaymentRequest!.contractAddress;
+ if (contractAddress != null && contractAddress.isNotEmpty) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ widget.sendViewModel.fetchTokenForContractAddress(contractAddress);
+ }
+ });
+ }
}
/// if the current wallet doesn't match the one in the qr code
diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart
index c0d74708..fdc48b1e 100644
--- a/lib/utils/payment_request.dart
+++ b/lib/utils/payment_request.dart
@@ -60,6 +60,16 @@ class PaymentRequest {
address = paymentUri.address;
amount = paymentUri.amount;
contractAddress = paymentUri.contractAddress;
+ } else if (scheme == "tron") {
+ final token = uri.queryParameters['token'];
+ if (token != null && token.isNotEmpty) {
+ contractAddress = token;
+ }
+ } else if (scheme == "solana") {
+ final splToken = uri.queryParameters['spl-token'];
+ if (splToken != null && splToken.isNotEmpty) {
+ contractAddress = splToken;
+ }
}
}
diff --git a/lib/utils/token_utilities.dart b/lib/utils/token_utilities.dart
index 8e70e3e2..34dc393a 100644
--- a/lib/utils/token_utilities.dart
+++ b/lib/utils/token_utilities.dart
@@ -144,7 +144,7 @@ class TokenUtilities {
return result;
}
- /// Finds a token by address across wallets depending on [walletType]
+ /// Finds a token by address for the given [walletType].
/// - EVM chains: match by contractAddress
/// - Solana: match by mintAddress
/// - Tron: match by contractAddress
@@ -152,34 +152,15 @@ class TokenUtilities {
required WalletType walletType,
required String address,
}) async {
- if(address.isEmpty) return null;
+ if (address.isEmpty) return null;
final lower = address.toLowerCase();
- switch (walletType) {
- case WalletType.ethereum:
- case WalletType.polygon:
- case WalletType.base:
- case WalletType.arbitrum:
- case WalletType.bsc:
- final tokens = await loadAllUniqueEvmTokens();
- for (final t in tokens) {
- if (t.contractAddress.toLowerCase() == lower) return t;
- }
- return null;
- case WalletType.solana:
- final solTokens = await loadAllUniqueSolTokens();
- for (final t in solTokens) {
- if (t.mintAddress.toLowerCase() == lower) return t;
- }
- return null;
- case WalletType.tron:
- final tronTokens = await loadAllUniqueTronTokens();
- for (final t in tronTokens) {
- if (t.contractAddress.toLowerCase() == lower) return t;
- }
- return null;
- default:
- return null;
+ final tokens = await getAvailableTokensForNetwork(walletType);
+ for (final t in tokens) {
+ if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) return t;
+ if (t is SPLToken && t.mintAddress.toLowerCase() == lower) return t;
+ if (t is TronToken && t.contractAddress.toLowerCase() == lower) return t;
}
+ return null;
}
static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
index 89c5374f..f097e5d1 100644
--- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
@@ -34,6 +34,8 @@ import 'package:cw_core/currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/payment_uris.dart';
+import 'package:cw_core/spl_token.dart';
+import 'package:cw_core/tron_token.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -202,6 +204,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
amount: _amount,
contractAddress: (tokenCurrency as Erc20Token).contractAddress);
}
+ if (tokenCurrency is TronToken && wallet.type == WalletType.tron) {
+ return TronURI(
+ amount: _amount,
+ address: wallet.walletAddresses.address,
+ contractAddress: (tokenCurrency as TronToken).contractAddress,
+ );
+ }
+ if (tokenCurrency is SPLToken && wallet.type == WalletType.solana) {
+ return SolanaURI(
+ amount: _amount,
+ address: wallet.walletAddresses.address,
+ contractAddress: (tokenCurrency as SPLToken).mintAddress,
+ );
+ }
if (isLightning && _lnPaymentRequest != null) return _lnPaymentRequest!;
return wallet.walletAddresses.getPaymentUri(_amount);
}
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.