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)

David Adegoke committed Jun 10, 2026 at 18:31 UTC 99f96b0a78965c2f1a19ef36ef1d140c91927cec
5 files changed +64 -31
cw_core/lib/payment_uris.dart
+22 -4
@@ -176,14 +176,23 @@ class PolygonURI extends PaymentURI {
176 }
177
178 class SolanaURI extends PaymentURI {
179 - SolanaURI({required super.amount, required super.address});
179 + SolanaURI({required super.amount, required super.address, this.contractAddress});
180 +
181 + final String? contractAddress;
182
183 @override
184 String toString() {
185 var base = 'solana:$address';
186 + final params = <String>[];
187
188 if (amount.isNotEmpty) {
186 - base += '?amount=${amount.replaceAll(',', '.')}';
189 + params.add('amount=${amount.replaceAll(',', '.')}');
190 + }
191 + if (contractAddress != null && contractAddress!.isNotEmpty) {
192 + params.add('spl-token=$contractAddress');
193 + }
194 + if (params.isNotEmpty) {
195 + base += '?${params.join('&')}';
196 }
197
198 return base;
@@ -191,14 +200,23 @@ class SolanaURI extends PaymentURI {
200 }
201
202 class TronURI extends PaymentURI {
194 - TronURI({required super.amount, required super.address});
203 + TronURI({required super.amount, required super.address, this.contractAddress});
204 +
205 + final String? contractAddress;
206
207 @override
208 String toString() {
209 var base = 'tron:$address';
210 + final params = <String>[];
211
212 if (amount.isNotEmpty) {
201 - base += '?amount=${amount.replaceAll(',', '.')}';
213 + params.add('amount=${amount.replaceAll(',', '.')}');
214 + }
215 + if (contractAddress != null && contractAddress!.isNotEmpty) {
216 + params.add('token=$contractAddress');
217 + }
218 + if (params.isNotEmpty) {
219 + base += '?${params.join('&')}';
220 }
221
222 return base;
lib/new-ui/pages/send_page.dart
+8
@@ -228,6 +228,14 @@ class _NewSendPageState extends State<NewSendPage> {
228 _addressControllers[0].text = widget.initialPaymentRequest!.address;
229 _amountControllers[0].text = widget.initialPaymentRequest!.amount;
230 _memoControllers[0].text = widget.initialPaymentRequest!.note;
231 + final contractAddress = widget.initialPaymentRequest!.contractAddress;
232 + if (contractAddress != null && contractAddress.isNotEmpty) {
233 + WidgetsBinding.instance.addPostFrameCallback((_) {
234 + if (mounted) {
235 + widget.sendViewModel.fetchTokenForContractAddress(contractAddress);
236 + }
237 + });
238 + }
239 }
240
241 /// if the current wallet doesn't match the one in the qr code
lib/utils/payment_request.dart
+10
@@ -60,6 +60,16 @@ class PaymentRequest {
60 address = paymentUri.address;
61 amount = paymentUri.amount;
62 contractAddress = paymentUri.contractAddress;
63 + } else if (scheme == "tron") {
64 + final token = uri.queryParameters['token'];
65 + if (token != null && token.isNotEmpty) {
66 + contractAddress = token;
67 + }
68 + } else if (scheme == "solana") {
69 + final splToken = uri.queryParameters['spl-token'];
70 + if (splToken != null && splToken.isNotEmpty) {
71 + contractAddress = splToken;
72 + }
73 }
74 }
75
lib/utils/token_utilities.dart
+8 -27
@@ -144,7 +144,7 @@ class TokenUtilities {
144 return result;
145 }
146
147 - /// Finds a token by address across wallets depending on [walletType]
147 + /// Finds a token by address for the given [walletType].
148 /// - EVM chains: match by contractAddress
149 /// - Solana: match by mintAddress
150 /// - Tron: match by contractAddress
@@ -152,34 +152,15 @@ class TokenUtilities {
152 required WalletType walletType,
153 required String address,
154 }) async {
155 - if(address.isEmpty) return null;
155 + if (address.isEmpty) return null;
156 final lower = address.toLowerCase();
157 - switch (walletType) {
158 - case WalletType.ethereum:
159 - case WalletType.polygon:
160 - case WalletType.base:
161 - case WalletType.arbitrum:
162 - case WalletType.bsc:
163 - final tokens = await loadAllUniqueEvmTokens();
164 - for (final t in tokens) {
165 - if (t.contractAddress.toLowerCase() == lower) return t;
166 - }
167 - return null;
168 - case WalletType.solana:
169 - final solTokens = await loadAllUniqueSolTokens();
170 - for (final t in solTokens) {
171 - if (t.mintAddress.toLowerCase() == lower) return t;
172 - }
173 - return null;
174 - case WalletType.tron:
175 - final tronTokens = await loadAllUniqueTronTokens();
176 - for (final t in tronTokens) {
177 - if (t.contractAddress.toLowerCase() == lower) return t;
178 - }
179 - return null;
180 - default:
181 - return null;
157 + final tokens = await getAvailableTokensForNetwork(walletType);
158 + for (final t in tokens) {
159 + if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) return t;
160 + if (t is SPLToken && t.mintAddress.toLowerCase() == lower) return t;
161 + if (t is TronToken && t.contractAddress.toLowerCase() == lower) return t;
162 }
163 + return null;
164 }
165
166 static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+16
@@ -34,6 +34,8 @@ import 'package:cw_core/currency.dart';
34 import 'package:cw_core/currency_for_wallet_type.dart';
35 import 'package:cw_core/erc20_token.dart';
36 import 'package:cw_core/payment_uris.dart';
37 +import 'package:cw_core/spl_token.dart';
38 +import 'package:cw_core/tron_token.dart';
39 import 'package:cw_core/wallet_type.dart';
40 import 'package:mobx/mobx.dart';
41
@@ -202,6 +204,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
204 amount: _amount,
205 contractAddress: (tokenCurrency as Erc20Token).contractAddress);
206 }
207 + if (tokenCurrency is TronToken && wallet.type == WalletType.tron) {
208 + return TronURI(
209 + amount: _amount,
210 + address: wallet.walletAddresses.address,
211 + contractAddress: (tokenCurrency as TronToken).contractAddress,
212 + );
213 + }
214 + if (tokenCurrency is SPLToken && wallet.type == WalletType.solana) {
215 + return SolanaURI(
216 + amount: _amount,
217 + address: wallet.walletAddresses.address,
218 + contractAddress: (tokenCurrency as SPLToken).mintAddress,
219 + );
220 + }
221 if (isLightning && _lnPaymentRequest != null) return _lnPaymentRequest!;
222 return wallet.walletAddresses.getPaymentUri(_amount);
223 }