Pay Anything Enhancements (#2529)

* fix: update Bitcoin and Litecoin Bech32 address patterns for improved detection for pay anything flow * feat: add Litecoin MWEB address detection patterns for pay anything flow * feat: Add support for ERC-681 payment scheme to QR and link to exchange send from external QR * feat: Introduce TokenUtilities to centralize token management across viewmodels - Added TokenUtilities class to centralize token-related operations for EVM, Solana, and Tron tokens. - Refactored existing methods in ExchangeTradeViewModel and SendViewModel to utilize TokenUtilities. - Updated PaymentRequest to include contractAddress. - Enhanced payment QR scanning to fetch token details for QRs with contract addresses - ERC-681 QR scheme * fix: Prefill amount with other data when app is brought up via payment deeplink on QR scan * fix: Reduce trade not created errors WIP * feat: Add exchange provider logging functionality * feat: Enhance ERC681URI with amount normalization, prevents parsing issues with various input formats for amounts * - Use regex from address validator for btc/ltc manual checks - Add check for solana addresses - Handle failures in normalizeToInteger method - fix issue with address only being added when there's an amount * wrap in try/catch just in case [skip ci] * Update lib/utils/token_utilities.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

David Adegoke committed Sep 24, 2025 at 18:24 UTC c51c2dc1057bb55f4f862743793d23123f5b8294
29 files changed +2921 -475
lib/core/payment_uris.dart new
+410
@@ -0,0 +1,410 @@
1 +import 'package:cw_core/format_fixed.dart';
2 +
3 +abstract class PaymentURI {
4 + PaymentURI({required this.amount, required this.address});
5 +
6 + final String amount;
7 + final String address;
8 +}
9 +
10 +class MoneroURI extends PaymentURI {
11 + MoneroURI({required super.amount, required super.address});
12 +
13 + @override
14 + String toString() {
15 + var base = 'monero:$address';
16 +
17 + if (amount.isNotEmpty) {
18 + base += '?tx_amount=${amount.replaceAll(',', '.')}';
19 + }
20 +
21 + return base;
22 + }
23 +}
24 +
25 +class HavenURI extends PaymentURI {
26 + HavenURI({required super.amount, required super.address});
27 +
28 + @override
29 + String toString() {
30 + var base = 'haven:$address';
31 +
32 + if (amount.isNotEmpty) {
33 + base += '?tx_amount=${amount.replaceAll(',', '.')}';
34 + }
35 +
36 + return base;
37 + }
38 +}
39 +
40 +class BitcoinURI extends PaymentURI {
41 + BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
42 +
43 + final String pjUri;
44 +
45 + @override
46 + String toString() {
47 + final qp = <String, String>{};
48 +
49 + if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
50 + if (pjUri.isNotEmpty && !address.startsWith("sp")) {
51 + qp['pjos'] = '0';
52 + qp['pj'] = pjUri;
53 + }
54 +
55 + return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
56 + }
57 +}
58 +
59 +class LitecoinURI extends PaymentURI {
60 + LitecoinURI({required super.amount, required super.address});
61 +
62 + @override
63 + String toString() {
64 + var base = 'litecoin:$address';
65 +
66 + if (amount.isNotEmpty) {
67 + base += '?amount=${amount.replaceAll(',', '.')}';
68 + }
69 +
70 + return base;
71 + }
72 +}
73 +
74 +class EthereumURI extends PaymentURI {
75 + EthereumURI({required super.amount, required super.address});
76 +
77 + @override
78 + String toString() {
79 + var base = 'ethereum:$address';
80 +
81 + if (amount.isNotEmpty) {
82 + base += '?amount=${amount.replaceAll(',', '.')}';
83 + }
84 +
85 + return base;
86 + }
87 +}
88 +
89 +class BitcoinCashURI extends PaymentURI {
90 + BitcoinCashURI({required super.amount, required super.address});
91 +
92 + @override
93 + String toString() {
94 + var base = address;
95 +
96 + if (amount.isNotEmpty) {
97 + base += '?amount=${amount.replaceAll(',', '.')}';
98 + }
99 +
100 + return base;
101 + }
102 +}
103 +
104 +class NanoURI extends PaymentURI {
105 + NanoURI({required super.amount, required super.address});
106 +
107 + @override
108 + String toString() {
109 + var base = 'nano:$address';
110 + if (amount.isNotEmpty) {
111 + base += '?amount=${amount.replaceAll(',', '.')}';
112 + }
113 +
114 + return base;
115 + }
116 +}
117 +
118 +class PolygonURI extends PaymentURI {
119 + PolygonURI({required super.amount, required super.address});
120 +
121 + @override
122 + String toString() {
123 + var base = 'polygon:$address';
124 +
125 + if (amount.isNotEmpty) {
126 + base += '?amount=${amount.replaceAll(',', '.')}';
127 + }
128 +
129 + return base;
130 + }
131 +}
132 +
133 +class SolanaURI extends PaymentURI {
134 + SolanaURI({required super.amount, required super.address});
135 +
136 + @override
137 + String toString() {
138 + var base = 'solana:$address';
139 +
140 + if (amount.isNotEmpty) {
141 + base += '?amount=${amount.replaceAll(',', '.')}';
142 + }
143 +
144 + return base;
145 + }
146 +}
147 +
148 +class TronURI extends PaymentURI {
149 + TronURI({required super.amount, required super.address});
150 +
151 + @override
152 + String toString() {
153 + var base = 'tron:$address';
154 +
155 + if (amount.isNotEmpty) {
156 + base += '?amount=${amount.replaceAll(',', '.')}';
157 + }
158 +
159 + return base;
160 + }
161 +}
162 +
163 +class WowneroURI extends PaymentURI {
164 + WowneroURI({required super.amount, required super.address});
165 +
166 + @override
167 + String toString() {
168 + var base = 'wownero:$address';
169 +
170 + if (amount.isNotEmpty) {
171 + base += '?tx_amount=${amount.replaceAll(',', '.')}';
172 + }
173 +
174 + return base;
175 + }
176 +}
177 +
178 +class ZanoURI extends PaymentURI {
179 + ZanoURI({required String amount, required String address})
180 + : super(amount: amount, address: address);
181 +
182 + @override
183 + String toString() {
184 + var base = 'zano:' + address;
185 +
186 + if (amount.isNotEmpty) {
187 + base += '?amount=${amount.replaceAll(',', '.')}';
188 + }
189 +
190 + return base;
191 + }
192 +}
193 +
194 +class DecredURI extends PaymentURI {
195 + DecredURI({required String amount, required String address})
196 + : super(amount: amount, address: address);
197 +
198 + @override
199 + String toString() {
200 + var base = 'decred:' + address;
201 +
202 + if (amount.isNotEmpty) {
203 + base += '?amount=${amount.replaceAll(',', '.')}';
204 + }
205 +
206 + return base;
207 + }
208 +}
209 +
210 +class DogeURI extends PaymentURI {
211 + DogeURI({required String amount, required String address})
212 + : super(amount: amount, address: address);
213 +
214 + @override
215 + String toString() {
216 + var base = 'doge:' + address;
217 +
218 + if (amount.isNotEmpty) {
219 + base += '?amount=${amount.replaceAll(',', '.')}';
220 + }
221 +
222 + return base;
223 + }
224 +}
225 +
226 +class ERC681URI extends PaymentURI {
227 + final int chainId;
228 + final String? contractAddress;
229 +
230 + ERC681URI({
231 + required this.chainId,
232 + required super.address,
233 + required super.amount,
234 + required this.contractAddress,
235 + });
236 +
237 + @override
238 + String toString() {
239 + var uri = 'ethereum:';
240 +
241 + final targetAddress = contractAddress ?? address;
242 + uri += targetAddress;
243 +
244 + if (chainId != 1) {
245 + uri += '@$chainId';
246 + }
247 +
248 + if (contractAddress != null) {
249 + uri += '/transfer';
250 + }
251 +
252 + final params = <String, String>{};
253 +
254 + if (contractAddress != null) {
255 + params['address'] = address;
256 + if (amount.isNotEmpty) {
257 + params['uint256'] = _formatAmountForERC20(amount);
258 + }
259 + } else {
260 + if (amount.isNotEmpty) {
261 + params['value'] = _formatAmountForNative(amount);
262 + }
263 + }
264 +
265 + if (params.isNotEmpty) {
266 + uri += '?';
267 + uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
268 + }
269 +
270 + return uri;
271 + }
272 +
273 + /// Formats amount for ERC-20 token transfers (in atomic units)
274 + String _formatAmountForERC20(String amount) {
275 + try {
276 + // Convert decimal amount to BigInt (assuming 18 decimals)
277 + final amountDouble = double.parse(amount.replaceAll(',', '.'));
278 + final amountBigInt = BigInt.from(amountDouble * 1e18);
279 + return amountBigInt.toString();
280 + } catch (e) {
281 + // Fallback to original amount if parsing fails
282 + return amount.replaceAll(',', '.');
283 + }
284 + }
285 +
286 + /// Formats amount for native ETH payments (in wei using scientific notation)
287 + String _formatAmountForNative(String amount) {
288 + try {
289 + // Convert decimal amount to double for scientific notation
290 + final amountDouble = double.parse(amount.replaceAll(',', '.'));
291 +
292 + // Use scientific notation as recommended by ERC-681
293 + return '${amountDouble}e18';
294 + } catch (e) {
295 + // Fallback to original amount if parsing fails
296 + return amount.replaceAll(',', '.');
297 + }
298 + }
299 +
300 + factory ERC681URI.fromUri(Uri uri) {
301 + final (isContract, targetAddress) = _getTargetAddress(uri.path);
302 + final chainId = _getChainID(uri.path);
303 +
304 + final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
305 + final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
306 +
307 + var formatedAmount = "";
308 +
309 + if (amountParam != null) {
310 + final normalized = _normalizeToIntegerWei(amountParam);
311 + formatedAmount = formatFixed(BigInt.parse(normalized), 18);
312 + } else {
313 + formatedAmount = uri.queryParameters["amount"] ?? "";
314 + }
315 +
316 + return ERC681URI(
317 + chainId: chainId,
318 + address: address,
319 + amount: formatedAmount,
320 + contractAddress: isContract ? targetAddress : null,
321 + );
322 + }
323 +
324 + static int _getChainID(String path) {
325 + return int.parse(RegExp(
326 + r'@\d*',
327 + ).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
328 + "1");
329 + }
330 +
331 + static (bool, String) _getTargetAddress(String path) {
332 + final targetAddress =
333 + RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!;
334 + return (path.contains("/"), targetAddress);
335 + }
336 +
337 + /// Normalize an input amount into an integer wei string.
338 + ///
339 + /// Accepts the following forms:
340 + /// - Integer string: "123000000000000000" → unchanged
341 + /// - Scientific notation: "0.123e18", "1e6" → expanded to integer
342 + /// - Decimal ETH: "0.123456" → shifted by 18 decimals
343 + static String _normalizeToIntegerWei(String input) {
344 + final raw = input.replaceAll(',', '.').trim();
345 +
346 + // First we check if it's already a plain integer (basically just a number with no dot, no exponent)
347 + try {
348 + final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) &&
349 + !raw.contains('.') &&
350 + !raw.toLowerCase().contains('e');
351 + if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), '');
352 +
353 + // Then we check if it's a scientific notation
354 + final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$');
355 + if (sci.hasMatch(raw)) {
356 + final mantissaStr = raw.toLowerCase().split('e')[0];
357 + final exp = int.parse(raw.toLowerCase().split('e')[1]);
358 + return _expandDecimal(mantissaStr, exp);
359 + }
360 +
361 + // Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount
362 + if (raw.contains('.')) {
363 + return _expandDecimal(raw, 18);
364 + }
365 + return raw;
366 + } catch (e) {
367 + return raw;
368 + }
369 +
370 + // If none of these checks work, we return the raw input
371 + }
372 +
373 + /// Expands a decimal string by shifting the decimal point `expShift` places
374 + /// to the right and returns an integer string (digits only, optional leading minus).
375 + /// Examples:
376 + /// _expandDecimal('0.123456', 18) -> '123456000000000000'
377 + /// _expandDecimal('1.2', 3) -> '1200'
378 + static String _expandDecimal(String decimalStr, int expShift) {
379 + var s = decimalStr.trim();
380 + var sign = '';
381 + if (s.startsWith('-') || s.startsWith('+')) {
382 + sign = s[0] == '-' ? '-' : '';
383 + s = s.substring(1);
384 + }
385 +
386 + // First we split the integer and fractional parts
387 + final parts = s.split('.');
388 + final intPart = parts[0].isEmpty ? '0' : parts[0];
389 + final fracPart = parts.length > 1 ? parts[1] : '';
390 + final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), '');
391 + final fracLen = fracPart.length;
392 +
393 + // Then we calculate the effective shift = desired shift minus existing fractional digits
394 + final shift = expShift - fracLen;
395 + if (shift >= 0) {
396 + final head = digits.isEmpty ? '0' : digits;
397 + final zeros = List.filled(shift, '0').join();
398 + final res = head + zeros;
399 + return sign + (res.isEmpty ? '0' : res);
400 + } else {
401 + // Need to insert a decimal point within digits; return integer by truncating
402 + final cut = digits.length + shift;
403 + if (cut <= 0) {
404 + return '0';
405 + }
406 + final res = digits.substring(0, cut);
407 + return sign + (res.isEmpty ? '0' : res);
408 + }
409 + }
410 +}
lib/core/universal_address_detector.dart
+25 -23
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/utils/payment_request.dart';
2 +import 'package:cake_wallet/core/address_validator.dart';
3 import 'package:cw_core/crypto_currency.dart';
4 import 'package:cw_core/wallet_type.dart';
5
@@ -104,15 +105,30 @@ class UniversalAddressDetector {
105 currency: CryptoCurrency.btcln,
106 ),
107
107 - // Bitcoin Bech32
108 + // Bitcoin (P2PKH, P2SH, Bech32, Silent Payments)
109 _DetectionPattern(
109 - pattern: RegExp(r'^bc1[a-km-zA-HJ-NP-Z1-9]{25,39}$'),
110 + pattern: RegExp('^(?:'
111 + '1[a-km-zA-HJ-NP-Z1-9]{25,34}'
112 + '|3[a-km-zA-HJ-NP-Z1-9]{25,34}'
113 + '|(?:bc|tb)1q[ac-hj-np-z02-9]{25,39}'
114 + '|(?:bc|tb)1p(?:[ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89})'
115 + '|(?:bc|tb)1q[ac-hj-np-z02-9]{40,80}'
116 + '|${AddressValidator.silentPaymentAddressPatternMainnet}'
117 + '|${AddressValidator.silentPaymentAddressPatternTestnet}'
118 + ')\$'),
119 currency: CryptoCurrency.btc,
120 ),
121
113 - // Litecoin Bech32
122 + // Litecoin (Legacy, Bech32, MWEB)
123 _DetectionPattern(
115 - pattern: RegExp(r'^ltc1[a-z0-9]{25,50}$'),
124 + pattern: RegExp('^(?:'
125 + 'L[a-km-zA-HJ-NP-Z1-9]{25,34}'
126 + '|[3M][a-km-zA-HJ-NP-Z1-9]{25,34}'
127 + '|ltc1q[ac-hj-np-z02-9]{25,39}'
128 + '|ltc1p(?:[ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89})'
129 + '|ltc1q[ac-hj-np-z02-9]{40,80}'
130 + '|${AddressValidator.mWebAddressPattern}'
131 + ')\$'),
132 currency: CryptoCurrency.ltc,
133 ),
134
@@ -182,31 +198,17 @@ class UniversalAddressDetector {
198 currency: CryptoCurrency.dcr,
199 ),
200
185 - // Bitcoin P2PKH/P2SH (legacy formats)
186 - _DetectionPattern(
187 - pattern: RegExp(r'^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$'),
188 - currency: CryptoCurrency.btc,
189 - ),
190 -
191 - // Litecoin P2PKH/P2SH
192 - _DetectionPattern(
193 - pattern: RegExp(r'^(L|M|3)[a-km-zA-HJ-NP-Z1-9]{25,34}$'),
194 - currency: CryptoCurrency.ltc,
195 - ),
196 -
201 // Dogecoin P2PKH
202 _DetectionPattern(
203 pattern: RegExp(r'^D[a-km-zA-HJ-NP-Z1-9]{25,34}$'),
204 currency: CryptoCurrency.doge,
205 ),
206
203 - // TODO: commented out until a better approach is implemented.
204 - // as this will consider most addresses to be a valid Solana address
205 - // Solana (Base58 format, 32-44 chars)
206 - // _DetectionPattern(
207 - // pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{32,44}$'),
208 - // currency: CryptoCurrency.sol,
209 - // ),
207 + // Solana (Base58 format)
208 + _DetectionPattern(
209 + pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{43,44}$'),
210 + currency: CryptoCurrency.sol,
211 + ),
212 ];
213
214 // Test each pattern in order of specificity
lib/di.dart
+6
@@ -36,6 +36,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
36 import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
37 import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
38 import 'package:cake_wallet/src/screens/dev/network_requests.dart';
39 +import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart';
40 import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
41 import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
42 import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart';
@@ -287,6 +288,7 @@ import 'package:cake_wallet/core/trade_monitor.dart';
288 import 'package:cake_wallet/core/reset_service.dart';
289 import 'package:cake_wallet/view_model/dev/socket_health_logs_view_model.dart';
290 import 'package:cake_wallet/src/screens/dev/socket_health_logs_page.dart';
291 +import 'package:cake_wallet/view_model/dev/exchange_provider_logs_view_model.dart';
292 import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
293 import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
294
@@ -818,6 +820,7 @@ Future<void> setup({
820 coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.nonMweb,
821 getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom),
822 getIt.get<FeesViewModel>(),
823 + _walletInfoSource,
824 ),
825 );
826
@@ -1566,6 +1569,9 @@ Future<void> setup({
1569
1570 getIt.registerFactory(() => DevNetworkRequests());
1571
1572 + getIt.registerFactory(() => ExchangeProviderLogsViewModel());
1573 + getIt.registerFactory(() => DevExchangeProviderLogsPage(getIt.get<ExchangeProviderLogsViewModel>()));
1574 +
1575 getIt.registerFactory(() => StartTorPage(StartTorViewModel(),));
1576
1577 getIt.registerFactory(() => DEuroViewModel(
lib/exchange/provider/chainflip_exchange_provider.dart
+77 -3
@@ -13,6 +13,7 @@ import 'package:cw_core/crypto_currency.dart';
13 import 'package:cw_core/utils/print_verbose.dart';
14 import 'package:hive/hive.dart';
15 import 'package:cw_core/utils/proxy_wrapper.dart';
16 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
17
18 class ChainflipExchangeProvider extends ExchangeProvider {
19 ChainflipExchangeProvider({required this.tradesStore})
@@ -114,8 +115,41 @@ class ChainflipExchangeProvider extends ExchangeProvider {
115 final expectedAmountOut =
116 quoteResponse['egressAmountNative'] as String? ?? '0';
117
117 - return _amountFromNative(expectedAmountOut, to) / amount;
118 - } catch (e) {
118 + final rate = _amountFromNative(expectedAmountOut, to) / amount;
119 +
120 + ExchangeProviderLogger.logSuccess(
121 + provider: description,
122 + function: 'fetchRate',
123 + requestData: {
124 + 'from': from.title,
125 + 'to': to.title,
126 + 'amount': amount,
127 + 'isFixedRateMode': isFixedRateMode,
128 + 'isReceiveAmount': isReceiveAmount,
129 + 'quoteParams': quoteParams,
130 + },
131 + responseData: {
132 + 'expectedAmountOut': expectedAmountOut,
133 + 'rate': rate,
134 + 'quoteResponse': quoteResponse,
135 + },
136 + );
137 +
138 + return rate;
139 + } catch (e, s) {
140 + ExchangeProviderLogger.logError(
141 + provider: description,
142 + function: 'fetchRate',
143 + error: e,
144 + stackTrace: s,
145 + requestData: {
146 + 'from': from.title,
147 + 'to': to.title,
148 + 'amount': amount,
149 + 'isFixedRateMode': isFixedRateMode,
150 + 'isReceiveAmount': isReceiveAmount,
151 + },
152 + );
153 printV(e.toString());
154 return 0.0;
155 }
@@ -164,6 +198,30 @@ class ChainflipExchangeProvider extends ExchangeProvider {
198
199 final id = '${swapResponse['issuedBlock']}-${swapResponse['network'].toString().toUpperCase()}-${swapResponse['channelId']}';
200
201 + ExchangeProviderLogger.logSuccess(
202 + provider: description,
203 + function: 'createTrade',
204 + requestData: {
205 + 'from': request.fromCurrency.title,
206 + 'to': request.toCurrency.title,
207 + 'fromAmount': request.fromAmount,
208 + 'toAmount': request.toAmount,
209 + 'toAddress': request.toAddress,
210 + 'refundAddress': request.refundAddress,
211 + 'isFixedRateMode': isFixedRateMode,
212 + 'isSendAll': isSendAll,
213 + 'quoteParams': quoteParams,
214 + 'swapParams': swapParams,
215 + },
216 + responseData: {
217 + 'id': id,
218 + 'inputAddress': swapResponse['address'].toString(),
219 + 'estimatedPrice': estimatedPrice,
220 + 'minimumPrice': minimumPrice,
221 + 'swapResponse': swapResponse,
222 + },
223 + );
224 +
225 return Trade(
226 id: id,
227 from: request.fromCurrency,
@@ -178,7 +236,23 @@ class ChainflipExchangeProvider extends ExchangeProvider {
236 userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
237 userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
238 isSendAll: isSendAll);
181 - } catch (e) {
239 + } catch (e, s) {
240 + ExchangeProviderLogger.logError(
241 + provider: description,
242 + function: 'createTrade',
243 + error: e,
244 + stackTrace: s,
245 + requestData: {
246 + 'from': request.fromCurrency.title,
247 + 'to': request.toCurrency.title,
248 + 'fromAmount': request.fromAmount,
249 + 'toAmount': request.toAmount,
250 + 'toAddress': request.toAddress,
251 + 'refundAddress': request.refundAddress,
252 + 'isFixedRateMode': isFixedRateMode,
253 + 'isSendAll': isSendAll,
254 + },
255 + );
256 printV(e.toString());
257 rethrow;
258 }
lib/exchange/provider/changenow_exchange_provider.dart
+38 -2
@@ -16,6 +16,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
16 import 'package:cake_wallet/wallet_type_utils.dart';
17 import 'package:cw_core/crypto_currency.dart';
18 import 'package:cw_core/utils/print_verbose.dart';
19 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
20
21 class ChangeNowExchangeProvider extends ExchangeProvider {
22 ChangeNowExchangeProvider({required SettingsStore settingsStore})
@@ -126,8 +127,43 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
127
128 if (rateId.isNotEmpty) _lastUsedRateId = rateId;
129
129 - return isReverse ? (amount / fromAmount) : (toAmount / amount);
130 - } catch (e) {
130 + final rate = isReverse ? (amount / fromAmount) : (toAmount / amount);
131 +
132 + ExchangeProviderLogger.logSuccess(
133 + provider: description,
134 + function: 'fetchRate',
135 + requestData: {
136 + 'from': from.title,
137 + 'to': to.title,
138 + 'amount': amount,
139 + 'isFixedRateMode': isFixedRateMode,
140 + 'isReceiveAmount': isReceiveAmount,
141 + 'type': type,
142 + 'flow': _getFlow(isFixedRateMode),
143 + },
144 + responseData: {
145 + 'fromAmount': fromAmount,
146 + 'toAmount': toAmount,
147 + 'rateId': rateId,
148 + 'rate': rate,
149 + },
150 + );
151 +
152 + return rate;
153 + } catch (e, s) {
154 + ExchangeProviderLogger.logError(
155 + provider: description,
156 + function: 'fetchRate',
157 + error: e,
158 + stackTrace: s,
159 + requestData: {
160 + 'from': from.title,
161 + 'to': to.title,
162 + 'amount': amount,
163 + 'isFixedRateMode': isFixedRateMode,
164 + 'isReceiveAmount': isReceiveAmount,
165 + },
166 + );
167 printV(e.toString());
168 return 0.0;
169 }
lib/exchange/provider/exolix_exchange_provider.dart
+122 -3
@@ -13,6 +13,7 @@ import 'package:cake_wallet/wallet_type_utils.dart';
13 import 'package:cw_core/utils/proxy_wrapper.dart';
14 import 'package:cw_core/crypto_currency.dart';
15 import 'package:cw_core/utils/print_verbose.dart';
16 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
17
18 class ExolixExchangeProvider extends ExchangeProvider {
19 ExolixExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -141,11 +142,62 @@ class ExolixExchangeProvider extends ExchangeProvider {
142
143 if (response.statusCode != 200) {
144 final message = responseJSON['message'] as String?;
145 +
146 + ExchangeProviderLogger.logError(
147 + provider: description,
148 + function: 'fetchRate',
149 + error: Exception(message ?? 'Unknown error'),
150 + stackTrace: StackTrace.current,
151 + requestData: {
152 + 'from': from.title,
153 + 'to': to.title,
154 + 'amount': amount,
155 + 'isFixedRateMode': isFixedRateMode,
156 + 'isReceiveAmount': isReceiveAmount,
157 + 'params': params,
158 + 'url': uri.toString(),
159 + },
160 + );
161 +
162 throw Exception(message);
163 }
164
147 - return responseJSON['rate'] as double;
148 - } catch (e) {
165 + final rate = responseJSON['rate'] as double;
166 +
167 + ExchangeProviderLogger.logSuccess(
168 + provider: description,
169 + function: 'fetchRate',
170 + requestData: {
171 + 'from': from.title,
172 + 'to': to.title,
173 + 'amount': amount,
174 + 'isFixedRateMode': isFixedRateMode,
175 + 'isReceiveAmount': isReceiveAmount,
176 + 'params': params,
177 + 'url': uri.toString(),
178 + },
179 + responseData: {
180 + 'rate': rate,
181 + 'statusCode': response.statusCode,
182 + 'responseJSON': responseJSON,
183 + },
184 + );
185 +
186 + return rate;
187 + } catch (e, s) {
188 + ExchangeProviderLogger.logError(
189 + provider: description,
190 + function: 'fetchRate',
191 + error: e,
192 + stackTrace: s,
193 + requestData: {
194 + 'from': from.title,
195 + 'to': to.title,
196 + 'amount': amount,
197 + 'isFixedRateMode': isFixedRateMode,
198 + 'isReceiveAmount': isReceiveAmount,
199 + },
200 + );
201 printV(e.toString());
202 return 0.0;
203 }
@@ -186,11 +238,50 @@ class ExolixExchangeProvider extends ExchangeProvider {
238 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
239 final errors = responseJSON['errors'] as Map<String, String>;
240 final errorMessage = errors.values.join(', ');
241 +
242 + ExchangeProviderLogger.logError(
243 + provider: description,
244 + function: 'createTrade',
245 + error: Exception(errorMessage),
246 + stackTrace: StackTrace.current,
247 + requestData: {
248 + 'from': request.fromCurrency.title,
249 + 'to': request.toCurrency.title,
250 + 'fromAmount': request.fromAmount,
251 + 'toAmount': request.toAmount,
252 + 'toAddress': request.toAddress,
253 + 'refundAddress': request.refundAddress,
254 + 'isFixedRateMode': isFixedRateMode,
255 + 'isSendAll': isSendAll,
256 + 'body': body,
257 + 'url': uri.toString(),
258 + },
259 + );
260 +
261 throw Exception(errorMessage);
262 }
263
192 - if (response.statusCode != 200 && response.statusCode != 201)
264 + if (response.statusCode != 200 && response.statusCode != 201) {
265 + ExchangeProviderLogger.logError(
266 + provider: description,
267 + function: 'createTrade',
268 + error: Exception('Unexpected http status: ${response.statusCode}'),
269 + stackTrace: StackTrace.current,
270 + requestData: {
271 + 'from': request.fromCurrency.title,
272 + 'to': request.toCurrency.title,
273 + 'fromAmount': request.fromAmount,
274 + 'toAmount': request.toAmount,
275 + 'toAddress': request.toAddress,
276 + 'refundAddress': request.refundAddress,
277 + 'isFixedRateMode': isFixedRateMode,
278 + 'isSendAll': isSendAll,
279 + 'body': body,
280 + 'url': uri.toString(),
281 + },
282 + );
283 throw Exception('Unexpected http status: ${response.statusCode}');
284 + }
285
286 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
287 final id = responseJSON['id'] as String;
@@ -201,6 +292,34 @@ class ExolixExchangeProvider extends ExchangeProvider {
292 final amount = responseJSON['amount'].toString();
293 final receiveAmount = responseJSON['amountTo']?.toString();
294
295 + ExchangeProviderLogger.logSuccess(
296 + provider: description,
297 + function: 'createTrade',
298 + requestData: {
299 + 'from': request.fromCurrency.title,
300 + 'to': request.toCurrency.title,
301 + 'fromAmount': request.fromAmount,
302 + 'toAmount': request.toAmount,
303 + 'toAddress': request.toAddress,
304 + 'refundAddress': request.refundAddress,
305 + 'isFixedRateMode': isFixedRateMode,
306 + 'isSendAll': isSendAll,
307 + 'body': body,
308 + 'url': uri.toString(),
309 + },
310 + responseData: {
311 + 'id': id,
312 + 'inputAddress': inputAddress,
313 + 'refundAddress': refundAddress,
314 + 'extraId': extraId,
315 + 'payoutAddress': payoutAddress,
316 + 'amount': amount,
317 + 'receiveAmount': receiveAmount,
318 + 'statusCode': response.statusCode,
319 + 'responseJSON': responseJSON,
320 + },
321 + );
322 +
323 return Trade(
324 id: id,
325 from: request.fromCurrency,
lib/exchange/provider/letsexchange_exchange_provider.dart
+113 -3
@@ -13,6 +13,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
13 import 'package:cw_core/utils/proxy_wrapper.dart';
14 import 'package:cw_core/crypto_currency.dart';
15 import 'package:cw_core/utils/print_verbose.dart';
16 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
17
18 class LetsExchangeExchangeProvider extends ExchangeProvider {
19 LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -102,8 +103,45 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
103
104 if (amountToGet == 0.0) return 0.0;
105
105 - return isFixedRateMode ? amount / amountToGet : amountToGet / amount;
106 - } catch (e) {
106 + final rate = isFixedRateMode ? amount / amountToGet : amountToGet / amount;
107 +
108 + ExchangeProviderLogger.logSuccess(
109 + provider: description,
110 + function: 'fetchRate',
111 + requestData: {
112 + 'from': from.title,
113 + 'to': to.title,
114 + 'amount': amount,
115 + 'isFixedRateMode': isFixedRateMode,
116 + 'isReceiveAmount': isReceiveAmount,
117 + 'networkFrom': networkFrom,
118 + 'networkTo': networkTo,
119 + 'params': params,
120 + },
121 + responseData: {
122 + 'amountToGet': amountToGet,
123 + 'rate': rate,
124 + 'responseJSON': responseJSON,
125 + },
126 + );
127 +
128 + return rate;
129 + } catch (e, s) {
130 + ExchangeProviderLogger.logError(
131 + provider: description,
132 + function: 'fetchRate',
133 + error: e,
134 + stackTrace: s,
135 + requestData: {
136 + 'from': from.title,
137 + 'to': to.title,
138 + 'amount': amount,
139 + 'isFixedRateMode': isFixedRateMode,
140 + 'isReceiveAmount': isReceiveAmount,
141 + 'networkFrom': networkFrom,
142 + 'networkTo': networkTo,
143 + },
144 + );
145 printV(e.toString());
146 return 0.0;
147 }
@@ -164,6 +202,26 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
202
203
204 if (response.statusCode != 200) {
205 + ExchangeProviderLogger.logError(
206 + provider: description,
207 + function: 'createTrade',
208 + error: Exception('LetsExchange create trade failed: ${response.body}'),
209 + stackTrace: StackTrace.current,
210 + requestData: {
211 + 'from': request.fromCurrency.title,
212 + 'to': request.toCurrency.title,
213 + 'fromAmount': request.fromAmount,
214 + 'toAmount': request.toAmount,
215 + 'toAddress': request.toAddress,
216 + 'refundAddress': request.refundAddress,
217 + 'isFixedRateMode': isFixedRateMode,
218 + 'isSendAll': isSendAll,
219 + 'networkFrom': networkFrom,
220 + 'networkTo': networkTo,
221 + 'tradeParams': tradeParams,
222 + 'url': uri.toString(),
223 + },
224 + );
225 throw Exception('LetsExchange create trade failed: ${response.body}');
226 }
227 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -197,6 +255,40 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
255 toCurrency = CryptoCurrency.fromString(to);
256 }
257
258 + ExchangeProviderLogger.logSuccess(
259 + provider: description,
260 + function: 'createTrade',
261 + requestData: {
262 + 'from': request.fromCurrency.title,
263 + 'to': request.toCurrency.title,
264 + 'fromAmount': request.fromAmount,
265 + 'toAmount': request.toAmount,
266 + 'toAddress': request.toAddress,
267 + 'refundAddress': request.refundAddress,
268 + 'isFixedRateMode': isFixedRateMode,
269 + 'isSendAll': isSendAll,
270 + 'networkFrom': networkFrom,
271 + 'networkTo': networkTo,
272 + 'tradeParams': tradeParams,
273 + 'url': uri.toString(),
274 + },
275 + responseData: {
276 + 'id': id,
277 + 'from': from,
278 + 'to': to,
279 + 'depositAddress': depositAddress,
280 + 'payoutAddress': payoutAddress,
281 + 'refundAddress': refundAddress,
282 + 'depositAmount': depositAmount,
283 + 'receiveAmount': receiveAmount,
284 + 'status': status,
285 + 'createdAt': createdAtString,
286 + 'expiredAt': expiredAtTimestamp,
287 + 'extraId': extraId,
288 + 'statusCode': response.statusCode,
289 + },
290 + );
291 +
292 return Trade(
293 id: id,
294 from: fromCurrency,
@@ -215,7 +307,25 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
307 userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
308 isSendAll: isSendAll,
309 );
218 - } catch (e) {
310 + } catch (e, s) {
311 + ExchangeProviderLogger.logError(
312 + provider: description,
313 + function: 'createTrade',
314 + error: e,
315 + stackTrace: s,
316 + requestData: {
317 + 'from': request.fromCurrency.title,
318 + 'to': request.toCurrency.title,
319 + 'fromAmount': request.fromAmount,
320 + 'toAmount': request.toAmount,
321 + 'toAddress': request.toAddress,
322 + 'refundAddress': request.refundAddress,
323 + 'isFixedRateMode': isFixedRateMode,
324 + 'isSendAll': isSendAll,
325 + 'networkFrom': networkFrom,
326 + 'networkTo': networkTo,
327 + },
328 + );
329 log(e.toString());
330 throw TradeNotCreatedException(description);
331 }
lib/exchange/provider/sideshift_exchange_provider.dart
+128 -2
@@ -13,6 +13,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
13 import 'package:cw_core/utils/proxy_wrapper.dart';
14 import 'package:cw_core/crypto_currency.dart';
15 import 'package:cw_core/utils/print_verbose.dart';
16 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
17
18 class SideShiftExchangeProvider extends ExchangeProvider {
19 SideShiftExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -145,15 +146,77 @@ class SideShiftExchangeProvider extends ExchangeProvider {
146 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
147 final error = responseJSON['error']['message'] as String;
148
149 + ExchangeProviderLogger.logError(
150 + provider: description,
151 + function: 'fetchRate',
152 + error: Exception('SideShift Internal Server Error: $error'),
153 + stackTrace: StackTrace.current,
154 + requestData: {
155 + 'from': from.title,
156 + 'to': to.title,
157 + 'amount': amount,
158 + 'isFixedRateMode': isFixedRateMode,
159 + 'isReceiveAmount': isReceiveAmount,
160 + 'url': url,
161 + },
162 + );
163 +
164 throw Exception('SideShift Internal Server Error: $error');
165 }
166
167 if (response.statusCode != 200) {
168 + ExchangeProviderLogger.logError(
169 + provider: description,
170 + function: 'fetchRate',
171 + error: Exception('Unexpected http status: ${response.statusCode}'),
172 + stackTrace: StackTrace.current,
173 + requestData: {
174 + 'from': from.title,
175 + 'to': to.title,
176 + 'amount': amount,
177 + 'isFixedRateMode': isFixedRateMode,
178 + 'isReceiveAmount': isReceiveAmount,
179 + 'url': url,
180 + },
181 + );
182 +
183 throw Exception('Unexpected http status: ${response.statusCode}');
184 }
185
155 - return double.parse(responseJSON['rate'] as String);
156 - } catch (e) {
186 + final rate = double.parse(responseJSON['rate'] as String);
187 +
188 + ExchangeProviderLogger.logSuccess(
189 + provider: description,
190 + function: 'fetchRate',
191 + requestData: {
192 + 'from': from.title,
193 + 'to': to.title,
194 + 'amount': amount,
195 + 'isFixedRateMode': isFixedRateMode,
196 + 'isReceiveAmount': isReceiveAmount,
197 + 'url': url,
198 + },
199 + responseData: {
200 + 'rate': rate,
201 + 'statusCode': response.statusCode,
202 + },
203 + );
204 +
205 + return rate;
206 + } catch (e, s) {
207 + ExchangeProviderLogger.logError(
208 + provider: description,
209 + function: 'fetchRate',
210 + error: e,
211 + stackTrace: s,
212 + requestData: {
213 + 'from': from.title,
214 + 'to': to.title,
215 + 'amount': amount,
216 + 'isFixedRateMode': isFixedRateMode,
217 + 'isReceiveAmount': isReceiveAmount,
218 + },
219 + );
220 printV(e.toString());
221 return 0.00;
222 }
@@ -199,9 +262,47 @@ class SideShiftExchangeProvider extends ExchangeProvider {
262 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
263 final error = responseJSON['error']['message'] as String;
264
265 + ExchangeProviderLogger.logError(
266 + provider: description,
267 + function: 'createTrade',
268 + error: TradeNotCreatedException(description, description: error),
269 + stackTrace: StackTrace.current,
270 + requestData: {
271 + 'from': request.fromCurrency.title,
272 + 'to': request.toCurrency.title,
273 + 'fromAmount': request.fromAmount,
274 + 'toAmount': request.toAmount,
275 + 'toAddress': request.toAddress,
276 + 'refundAddress': request.refundAddress,
277 + 'isFixedRateMode': isFixedRateMode,
278 + 'isSendAll': isSendAll,
279 + 'url': url,
280 + 'body': body,
281 + },
282 + );
283 +
284 throw TradeNotCreatedException(description, description: error);
285 }
286
287 + ExchangeProviderLogger.logError(
288 + provider: description,
289 + function: 'createTrade',
290 + error: TradeNotCreatedException(description),
291 + stackTrace: StackTrace.current,
292 + requestData: {
293 + 'from': request.fromCurrency.title,
294 + 'to': request.toCurrency.title,
295 + 'fromAmount': request.fromAmount,
296 + 'toAmount': request.toAmount,
297 + 'toAddress': request.toAddress,
298 + 'refundAddress': request.refundAddress,
299 + 'isFixedRateMode': isFixedRateMode,
300 + 'isSendAll': isSendAll,
301 + 'url': url,
302 + 'body': body,
303 + },
304 + );
305 +
306 throw TradeNotCreatedException(description);
307 }
308
@@ -212,6 +313,31 @@ class SideShiftExchangeProvider extends ExchangeProvider {
313 final depositAmount = responseJSON['depositAmount'] as String?;
314 final depositMemo = responseJSON['depositMemo'] as String?;
315
316 + ExchangeProviderLogger.logSuccess(
317 + provider: description,
318 + function: 'createTrade',
319 + requestData: {
320 + 'from': request.fromCurrency.title,
321 + 'to': request.toCurrency.title,
322 + 'fromAmount': request.fromAmount,
323 + 'toAmount': request.toAmount,
324 + 'toAddress': request.toAddress,
325 + 'refundAddress': request.refundAddress,
326 + 'isFixedRateMode': isFixedRateMode,
327 + 'isSendAll': isSendAll,
328 + 'url': url,
329 + 'body': body,
330 + },
331 + responseData: {
332 + 'id': id,
333 + 'inputAddress': inputAddress,
334 + 'settleAddress': settleAddress,
335 + 'depositAmount': depositAmount,
336 + 'depositMemo': depositMemo,
337 + 'statusCode': response.statusCode,
338 + },
339 + );
340 +
341 return Trade(
342 id: id,
343 provider: description,
lib/exchange/provider/simpleswap_exchange_provider.dart
+122 -4
@@ -13,6 +13,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
13 import 'package:cake_wallet/utils/device_info.dart';
14 import 'package:cw_core/utils/proxy_wrapper.dart';
15 import 'package:cw_core/crypto_currency.dart';
16 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
17
18 class SimpleSwapExchangeProvider extends ExchangeProvider {
19 SimpleSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -108,11 +109,63 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
109 final response = await ProxyWrapper().get(clearnetUri: uri);
110
111
111 - if (response.body == "null") return 0.00;
112 + if (response.body == "null") {
113 + ExchangeProviderLogger.logError(
114 + provider: description,
115 + function: 'fetchRate',
116 + error: Exception('Null response body'),
117 + stackTrace: StackTrace.current,
118 + requestData: {
119 + 'from': from.title,
120 + 'to': to.title,
121 + 'amount': amount,
122 + 'isFixedRateMode': isFixedRateMode,
123 + 'isReceiveAmount': isReceiveAmount,
124 + 'params': params,
125 + 'url': uri.toString(),
126 + },
127 + );
128 + return 0.00;
129 + }
130 +
131 final data = json.decode(response.body) as String;
113 -
114 - return double.parse(data) / amount;
115 - } catch (_) {
132 + final rate = double.parse(data) / amount;
133 +
134 + ExchangeProviderLogger.logSuccess(
135 + provider: description,
136 + function: 'fetchRate',
137 + requestData: {
138 + 'from': from.title,
139 + 'to': to.title,
140 + 'amount': amount,
141 + 'isFixedRateMode': isFixedRateMode,
142 + 'isReceiveAmount': isReceiveAmount,
143 + 'params': params,
144 + 'url': uri.toString(),
145 + },
146 + responseData: {
147 + 'data': data,
148 + 'rate': rate,
149 + 'statusCode': response.statusCode,
150 + 'responseBody': response.body,
151 + },
152 + );
153 +
154 + return rate;
155 + } catch (e, s) {
156 + ExchangeProviderLogger.logError(
157 + provider: description,
158 + function: 'fetchRate',
159 + error: e,
160 + stackTrace: s,
161 + requestData: {
162 + 'from': from.title,
163 + 'to': to.title,
164 + 'amount': amount,
165 + 'isFixedRateMode': isFixedRateMode,
166 + 'isReceiveAmount': isReceiveAmount,
167 + },
168 + );
169 return 0.00;
170 }
171 }
@@ -147,9 +200,47 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
200 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
201 final error = responseJSON['message'] as String;
202
203 + ExchangeProviderLogger.logError(
204 + provider: description,
205 + function: 'createTrade',
206 + error: TradeNotCreatedException(description, description: error),
207 + stackTrace: StackTrace.current,
208 + requestData: {
209 + 'from': request.fromCurrency.title,
210 + 'to': request.toCurrency.title,
211 + 'fromAmount': request.fromAmount,
212 + 'toAmount': request.toAmount,
213 + 'toAddress': request.toAddress,
214 + 'refundAddress': request.refundAddress,
215 + 'isFixedRateMode': isFixedRateMode,
216 + 'isSendAll': isSendAll,
217 + 'body': body,
218 + 'url': uri.toString(),
219 + },
220 + );
221 +
222 throw TradeNotCreatedException(description, description: error);
223 }
224
225 + ExchangeProviderLogger.logError(
226 + provider: description,
227 + function: 'createTrade',
228 + error: TradeNotCreatedException(description),
229 + stackTrace: StackTrace.current,
230 + requestData: {
231 + 'from': request.fromCurrency.title,
232 + 'to': request.toCurrency.title,
233 + 'fromAmount': request.fromAmount,
234 + 'toAmount': request.toAmount,
235 + 'toAddress': request.toAddress,
236 + 'refundAddress': request.refundAddress,
237 + 'isFixedRateMode': isFixedRateMode,
238 + 'isSendAll': isSendAll,
239 + 'body': body,
240 + 'url': uri.toString(),
241 + },
242 + );
243 +
244 throw TradeNotCreatedException(description);
245 }
246
@@ -161,6 +252,33 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
252 final extraId = responseJSON['extra_id_from'] as String?;
253 final receiveAmount = responseJSON['amount_to'] as String?;
254
255 + ExchangeProviderLogger.logSuccess(
256 + provider: description,
257 + function: 'createTrade',
258 + requestData: {
259 + 'from': request.fromCurrency.title,
260 + 'to': request.toCurrency.title,
261 + 'fromAmount': request.fromAmount,
262 + 'toAmount': request.toAmount,
263 + 'toAddress': request.toAddress,
264 + 'refundAddress': request.refundAddress,
265 + 'isFixedRateMode': isFixedRateMode,
266 + 'isSendAll': isSendAll,
267 + 'body': body,
268 + 'url': uri.toString(),
269 + },
270 + responseData: {
271 + 'id': id,
272 + 'inputAddress': inputAddress,
273 + 'payoutAddress': payoutAddress,
274 + 'settleAddress': settleAddress,
275 + 'extraId': extraId,
276 + 'receiveAmount': receiveAmount,
277 + 'statusCode': response.statusCode,
278 + 'responseJSON': responseJSON,
279 + },
280 + );
281 +
282 return Trade(
283 id: id,
284 provider: description,
lib/exchange/provider/stealth_ex_exchange_provider.dart
+142 -24
@@ -12,6 +12,7 @@ import 'package:cake_wallet/exchange/trade_state.dart';
12 import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
13 import 'package:cw_core/utils/proxy_wrapper.dart';
14 import 'package:cw_core/crypto_currency.dart';
15 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
16
17 class StealthExExchangeProvider extends ExchangeProvider {
18 StealthExExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -68,7 +69,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
69 headers: headers,
70 body: json.encode(body),
71 );
71 -
72 +
73 if (response.statusCode != 200) {
74 throw Exception('StealthEx fetch limits failed: ${response.body}');
75 }
@@ -89,14 +90,68 @@ class StealthExExchangeProvider extends ExchangeProvider {
90 required double amount,
91 required bool isFixedRateMode,
92 required bool isReceiveAmount}) async {
92 - final response = await getEstimatedExchangeAmount(
93 - from: from, to: to, amount: amount, isFixedRateMode: isFixedRateMode);
94 - final estimatedAmount = response['estimated_amount'] as double? ?? 0.0;
95 - return estimatedAmount > 0.0
96 - ? isFixedRateMode
97 - ? amount / estimatedAmount
98 - : estimatedAmount / amount
99 - : 0.0;
93 + try {
94 + final response = await getEstimatedExchangeAmount(
95 + from: from,
96 + to: to,
97 + amount: amount,
98 + isFixedRateMode: isFixedRateMode,
99 + );
100 + final estimatedAmount = response['estimated_amount'] as double? ?? 0.0;
101 +
102 + if (estimatedAmount <= 0.0) {
103 + ExchangeProviderLogger.logError(
104 + provider: description,
105 + function: 'fetchRate',
106 + error: Exception('Invalid estimated amount: $estimatedAmount'),
107 + stackTrace: StackTrace.current,
108 + requestData: {
109 + 'from': from.title,
110 + 'to': to.title,
111 + 'amount': amount,
112 + 'isFixedRateMode': isFixedRateMode,
113 + 'isReceiveAmount': isReceiveAmount,
114 + },
115 + );
116 + return 0.0;
117 + }
118 +
119 + final rate = isFixedRateMode ? amount / estimatedAmount : estimatedAmount / amount;
120 +
121 + ExchangeProviderLogger.logSuccess(
122 + provider: description,
123 + function: 'fetchRate',
124 + requestData: {
125 + 'from': from.title,
126 + 'to': to.title,
127 + 'amount': amount,
128 + 'isFixedRateMode': isFixedRateMode,
129 + 'isReceiveAmount': isReceiveAmount,
130 + },
131 + responseData: {
132 + 'estimatedAmount': estimatedAmount,
133 + 'rate': rate,
134 + 'response': response,
135 + },
136 + );
137 +
138 + return rate;
139 + } catch (e, s) {
140 + ExchangeProviderLogger.logError(
141 + provider: description,
142 + function: 'fetchRate',
143 + error: e,
144 + stackTrace: s,
145 + requestData: {
146 + 'from': from.title,
147 + 'to': to.title,
148 + 'amount': amount,
149 + 'isFixedRateMode': isFixedRateMode,
150 + 'isReceiveAmount': isReceiveAmount,
151 + },
152 + );
153 + return 0.0;
154 + }
155 }
156
157 @override
@@ -143,9 +198,26 @@ class StealthExExchangeProvider extends ExchangeProvider {
198 headers: headers,
199 body: json.encode(body),
200 );
146 -
201
202 if (response.statusCode != 201) {
203 + ExchangeProviderLogger.logError(
204 + provider: description,
205 + function: 'createTrade',
206 + error: Exception('StealthEx create trade failed: ${response.body}'),
207 + stackTrace: StackTrace.current,
208 + requestData: {
209 + 'from': request.fromCurrency.title,
210 + 'to': request.toCurrency.title,
211 + 'fromAmount': request.fromAmount,
212 + 'toAmount': request.toAmount,
213 + 'toAddress': request.toAddress,
214 + 'refundAddress': request.refundAddress,
215 + 'isFixedRateMode': isFixedRateMode,
216 + 'isSendAll': isSendAll,
217 + 'body': body,
218 + 'rateId': rateId,
219 + },
220 + );
221 throw Exception('StealthEx create trade failed: ${response.body}');
222 }
223 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -169,20 +241,51 @@ class StealthExExchangeProvider extends ExchangeProvider {
241 ? DateTime.parse(validUntil).toLocal()
242 : DateTime.now().add(Duration(minutes: 5));
243
172 -
244 CryptoCurrency fromCurrency;
245 if (request.fromCurrency.tag != null && request.fromCurrency.title.toLowerCase() == from) {
175 - fromCurrency = request.fromCurrency;
176 - } else {
177 - fromCurrency = CryptoCurrency.fromString(from);
178 - }
246 + fromCurrency = request.fromCurrency;
247 + } else {
248 + fromCurrency = CryptoCurrency.fromString(from);
249 + }
250
251 CryptoCurrency toCurrency;
252 if (request.toCurrency.tag != null && request.toCurrency.title.toLowerCase() == to) {
182 - toCurrency = request.toCurrency;
183 - } else {
184 - toCurrency = CryptoCurrency.fromString(to);
185 - }
253 + toCurrency = request.toCurrency;
254 + } else {
255 + toCurrency = CryptoCurrency.fromString(to);
256 + }
257 +
258 + ExchangeProviderLogger.logSuccess(
259 + provider: description,
260 + function: 'createTrade',
261 + requestData: {
262 + 'from': request.fromCurrency.title,
263 + 'to': request.toCurrency.title,
264 + 'fromAmount': request.fromAmount,
265 + 'toAmount': request.toAmount,
266 + 'toAddress': request.toAddress,
267 + 'refundAddress': request.refundAddress,
268 + 'isFixedRateMode': isFixedRateMode,
269 + 'isSendAll': isSendAll,
270 + 'body': body,
271 + 'rateId': rateId,
272 + },
273 + responseData: {
274 + 'id': id,
275 + 'from': from,
276 + 'to': to,
277 + 'depositAddress': depositAddress,
278 + 'payoutAddress': payoutAddress,
279 + 'refundAddress': refundAddress,
280 + 'depositAmount': depositAmount,
281 + 'receiveAmount': receiveAmount,
282 + 'status': status,
283 + 'createdAt': createdAtString,
284 + 'extraId': extraId,
285 + 'statusCode': response.statusCode,
286 + 'responseJSON': responseJSON,
287 + },
288 + );
289
290 return Trade(
291 id: id,
@@ -202,7 +305,23 @@ class StealthExExchangeProvider extends ExchangeProvider {
305 userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
306 isSendAll: isSendAll,
307 );
205 - } catch (e) {
308 + } catch (e, s) {
309 + ExchangeProviderLogger.logError(
310 + provider: description,
311 + function: 'createTrade',
312 + error: e,
313 + stackTrace: s,
314 + requestData: {
315 + 'from': request.fromCurrency.title,
316 + 'to': request.toCurrency.title,
317 + 'fromAmount': request.fromAmount,
318 + 'toAmount': request.toAmount,
319 + 'toAddress': request.toAddress,
320 + 'refundAddress': request.refundAddress,
321 + 'isFixedRateMode': isFixedRateMode,
322 + 'isSendAll': isSendAll,
323 + },
324 + );
325 log(e.toString());
326 throw TradeNotCreatedException(description);
327 }
@@ -214,8 +333,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
333
334 final uri = Uri.parse('$_baseUrl$_exchangesPath/$id');
335 final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
217 -
218 -
336 +
337 if (response.statusCode != 200) {
338 throw Exception('StealthEx fetch trade failed: ${response.body}');
339 }
@@ -272,7 +390,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
390 'estimation': isFixedRateMode ? 'reversed' : 'direct',
391 'rate': isFixedRateMode ? 'fixed' : 'floating',
392 'amount': amount,
275 - 'additional_fee_percent': _additionalFeePercent,
393 + 'additional_fee_percent': _additionalFeePercent,
394 };
395
396 try {
@@ -281,7 +399,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
399 headers: headers,
400 body: json.encode(body),
401 );
284 -
402 +
403 if (response.statusCode != 200) return {};
404 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
405 final rate = responseJSON['rate'] as Map<String, dynamic>?;
lib/exchange/provider/swaptrade_exchange_provider.dart
+136 -5
@@ -13,6 +13,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
13 import 'package:cw_core/utils/proxy_wrapper.dart';
14 import 'package:cw_core/crypto_currency.dart';
15 import 'package:cw_core/utils/print_verbose.dart';
16 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
17
18 class SwapTradeExchangeProvider extends ExchangeProvider {
19 SwapTradeExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -126,13 +127,64 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
127
128 final responseBody = json.decode(response.body) as Map<String, dynamic>;
129
129 - if (response.statusCode != 200)
130 + if (response.statusCode != 200) {
131 + ExchangeProviderLogger.logError(
132 + provider: description,
133 + function: 'fetchRate',
134 + error: Exception('Unexpected http status: ${response.statusCode}'),
135 + stackTrace: StackTrace.current,
136 + requestData: {
137 + 'from': from.title,
138 + 'to': to.title,
139 + 'amount': amount,
140 + 'isFixedRateMode': isFixedRateMode,
141 + 'isReceiveAmount': isReceiveAmount,
142 + 'body': body,
143 + 'url': uri.toString(),
144 + },
145 + );
146 throw Exception('Unexpected http status: ${response.statusCode}');
147 + }
148
149 final data = responseBody['data'] as Map<String, dynamic>;
150 double rate = double.parse(data['price'].toString());
134 - return rate > 0 ? isFixedRateMode ? amount / rate : rate / amount : 0.0;
135 - } catch (e) {
151 + final calculatedRate = rate > 0 ? isFixedRateMode ? amount / rate : rate / amount : 0.0;
152 +
153 + ExchangeProviderLogger.logSuccess(
154 + provider: description,
155 + function: 'fetchRate',
156 + requestData: {
157 + 'from': from.title,
158 + 'to': to.title,
159 + 'amount': amount,
160 + 'isFixedRateMode': isFixedRateMode,
161 + 'isReceiveAmount': isReceiveAmount,
162 + 'body': body,
163 + 'url': uri.toString(),
164 + },
165 + responseData: {
166 + 'rate': rate,
167 + 'calculatedRate': calculatedRate,
168 + 'statusCode': response.statusCode,
169 + 'responseBody': responseBody,
170 + },
171 + );
172 +
173 + return calculatedRate;
174 + } catch (e, s) {
175 + ExchangeProviderLogger.logError(
176 + provider: description,
177 + function: 'fetchRate',
178 + error: e,
179 + stackTrace: s,
180 + requestData: {
181 + 'from': from.title,
182 + 'to': to.title,
183 + 'amount': amount,
184 + 'isFixedRateMode': isFixedRateMode,
185 + 'isReceiveAmount': isReceiveAmount,
186 + },
187 + );
188 printV("error fetching rate: ${e.toString()}");
189 return 0.0;
190 }
@@ -169,15 +221,78 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
221
222 if (response.statusCode == 400 || responseBody["success"] == false) {
223 final error = responseBody['errors'][0]['msg'] as String;
224 +
225 + ExchangeProviderLogger.logError(
226 + provider: description,
227 + function: 'createTrade',
228 + error: TradeNotCreatedException(description, description: error),
229 + stackTrace: StackTrace.current,
230 + requestData: {
231 + 'from': request.fromCurrency.title,
232 + 'to': request.toCurrency.title,
233 + 'fromAmount': request.fromAmount,
234 + 'toAmount': request.toAmount,
235 + 'toAddress': request.toAddress,
236 + 'refundAddress': request.refundAddress,
237 + 'isFixedRateMode': isFixedRateMode,
238 + 'isSendAll': isSendAll,
239 + 'body': body,
240 + 'url': uri.toString(),
241 + },
242 + );
243 +
244 throw TradeNotCreatedException(description, description: error);
245 }
246
175 - if (response.statusCode != 200)
247 + if (response.statusCode != 200) {
248 + ExchangeProviderLogger.logError(
249 + provider: description,
250 + function: 'createTrade',
251 + error: Exception('Unexpected http status: ${response.statusCode}'),
252 + stackTrace: StackTrace.current,
253 + requestData: {
254 + 'from': request.fromCurrency.title,
255 + 'to': request.toCurrency.title,
256 + 'fromAmount': request.fromAmount,
257 + 'toAmount': request.toAmount,
258 + 'toAddress': request.toAddress,
259 + 'refundAddress': request.refundAddress,
260 + 'isFixedRateMode': isFixedRateMode,
261 + 'isSendAll': isSendAll,
262 + 'body': body,
263 + 'url': uri.toString(),
264 + },
265 + );
266 throw Exception('Unexpected http status: ${response.statusCode}');
267 + }
268
269 final responseData = responseBody['data'] as Map<String, dynamic>;
270 final receiveAmount = responseData["amount_receive"]?.toString();
271
272 + ExchangeProviderLogger.logSuccess(
273 + provider: description,
274 + function: 'createTrade',
275 + requestData: {
276 + 'from': request.fromCurrency.title,
277 + 'to': request.toCurrency.title,
278 + 'fromAmount': request.fromAmount,
279 + 'toAmount': request.toAmount,
280 + 'toAddress': request.toAddress,
281 + 'refundAddress': request.refundAddress,
282 + 'isFixedRateMode': isFixedRateMode,
283 + 'isSendAll': isSendAll,
284 + 'body': body,
285 + 'url': uri.toString(),
286 + },
287 + responseData: {
288 + 'id': responseData["order_id"] as String,
289 + 'inputAddress': responseData["server_address"] as String,
290 + 'receiveAmount': receiveAmount,
291 + 'statusCode': response.statusCode,
292 + 'responseBody': responseBody,
293 + },
294 + );
295 +
296 return Trade(
297 id: responseData["order_id"] as String,
298 inputAddress: responseData["server_address"] as String,
@@ -193,7 +308,23 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
308 userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
309 userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
310 );
196 - } catch (e) {
311 + } catch (e, s) {
312 + ExchangeProviderLogger.logError(
313 + provider: description,
314 + function: 'createTrade',
315 + error: e,
316 + stackTrace: s,
317 + requestData: {
318 + 'from': request.fromCurrency.title,
319 + 'to': request.toCurrency.title,
320 + 'fromAmount': request.fromAmount,
321 + 'toAmount': request.toAmount,
322 + 'toAddress': request.toAddress,
323 + 'refundAddress': request.refundAddress,
324 + 'isFixedRateMode': isFixedRateMode,
325 + 'isSendAll': isSendAll,
326 + },
327 + );
328 printV("error creating trade: ${e.toString()}");
329 throw TradeNotCreatedException(description, description: e.toString());
330 }
lib/exchange/provider/thorchain_exchange.provider.dart
+79 -3
@@ -11,6 +11,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
11 import 'package:cw_core/crypto_currency.dart';
12 import 'package:cw_core/utils/print_verbose.dart';
13 import 'package:hive/hive.dart';
14 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
15
16 class ThorChainExchangeProvider extends ExchangeProvider {
17 ThorChainExchangeProvider({required this.tradesStore})
@@ -85,9 +86,41 @@ class ThorChainExchangeProvider extends ExchangeProvider {
86 final responseJSON = await _getSwapQuote(params);
87
88 final expectedAmountOut = responseJSON['expected_amount_out'] as String? ?? '0.0';
88 -
89 - return _thorChainAmountToDouble(expectedAmountOut) / amount;
90 - } catch (e) {
89 + final rate = _thorChainAmountToDouble(expectedAmountOut) / amount;
90 +
91 + ExchangeProviderLogger.logSuccess(
92 + provider: description,
93 + function: 'fetchRate',
94 + requestData: {
95 + 'from': from.title,
96 + 'to': to.title,
97 + 'amount': amount,
98 + 'isFixedRateMode': isFixedRateMode,
99 + 'isReceiveAmount': isReceiveAmount,
100 + 'params': params,
101 + },
102 + responseData: {
103 + 'expectedAmountOut': expectedAmountOut,
104 + 'rate': rate,
105 + 'responseJSON': responseJSON,
106 + },
107 + );
108 +
109 + return rate;
110 + } catch (e, s) {
111 + ExchangeProviderLogger.logError(
112 + provider: description,
113 + function: 'fetchRate',
114 + error: e,
115 + stackTrace: s,
116 + requestData: {
117 + 'from': from.title,
118 + 'to': to.title,
119 + 'amount': amount,
120 + 'isFixedRateMode': isFixedRateMode,
121 + 'isReceiveAmount': isReceiveAmount,
122 + },
123 + );
124 printV(e.toString());
125 return 0.0;
126 }
@@ -144,6 +177,29 @@ class ThorChainExchangeProvider extends ExchangeProvider {
177 receiveAmount = _thorChainAmountToDouble(directAmountOutResponse).toString();
178 }
179
180 + ExchangeProviderLogger.logSuccess(
181 + provider: description,
182 + function: 'createTrade',
183 + requestData: {
184 + 'from': request.fromCurrency.title,
185 + 'to': request.toCurrency.title,
186 + 'fromAmount': request.fromAmount,
187 + 'toAmount': request.toAmount,
188 + 'toAddress': request.toAddress,
189 + 'refundAddress': request.refundAddress,
190 + 'isFixedRateMode': isFixedRateMode,
191 + 'isSendAll': isSendAll,
192 + 'params': params,
193 + },
194 + responseData: {
195 + 'inputAddress': inputAddress,
196 + 'memo': memo,
197 + 'directAmountOutResponse': directAmountOutResponse,
198 + 'receiveAmount': receiveAmount,
199 + 'responseJSON': responseJSON,
200 + },
201 + );
202 +
203 return Trade(
204 id: '',
205 from: request.fromCurrency,
@@ -253,10 +309,30 @@ class ThorChainExchangeProvider extends ExchangeProvider {
309 final response = await ProxyWrapper().get(clearnetUri: uri);
310
311 if (response.statusCode != 200) {
312 + ExchangeProviderLogger.logError(
313 + provider: description,
314 + function: '_getSwapQuote',
315 + error: Exception('Unexpected HTTP status: ${response.statusCode}'),
316 + stackTrace: StackTrace.current,
317 + requestData: {
318 + 'params': params,
319 + 'url': uri.toString(),
320 + },
321 + );
322 throw Exception('Unexpected HTTP status: ${response.statusCode}');
323 }
324
325 if (response.body.contains('error')) {
326 + ExchangeProviderLogger.logError(
327 + provider: description,
328 + function: '_getSwapQuote',
329 + error: Exception('Unexpected response: ${response.body}'),
330 + stackTrace: StackTrace.current,
331 + requestData: {
332 + 'params': params,
333 + 'url': uri.toString(),
334 + },
335 + );
336 throw Exception('Unexpected response: ${response.body}');
337 }
338
lib/exchange/provider/trocador_exchange_provider.dart
+146 -3
@@ -12,6 +12,7 @@ import 'package:cake_wallet/wallet_type_utils.dart';
12 import 'package:cw_core/utils/proxy_wrapper.dart';
13 import 'package:cw_core/crypto_currency.dart';
14 import 'package:cw_core/utils/print_verbose.dart';
15 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
16
17 class TrocadorExchangeProvider extends ExchangeProvider {
18 TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}})
@@ -156,13 +157,66 @@ class TrocadorExchangeProvider extends ExchangeProvider {
157 .toList();
158
159 if (_provider.isEmpty) {
160 + ExchangeProviderLogger.logError(
161 + provider: description,
162 + function: 'fetchRate',
163 + error: Exception('No enabled providers found for the selected trade.'),
164 + stackTrace: StackTrace.current,
165 + requestData: {
166 + 'from': from.title,
167 + 'to': to.title,
168 + 'amount': amount,
169 + 'isFixedRateMode': isFixedRateMode,
170 + 'isReceiveAmount': isReceiveAmount,
171 + 'params': params,
172 + 'url': uri.toString(),
173 + },
174 + );
175 throw Exception('No enabled providers found for the selected trade.');
176 }
177
178 if (rateId.isNotEmpty) _lastUsedRateId = rateId;
179
164 - return isReceiveAmount ? (amount / fromAmount) : (toAmount / amount);
165 - } catch (e) {
180 + final rate = isReceiveAmount ? (amount / fromAmount) : (toAmount / amount);
181 +
182 + ExchangeProviderLogger.logSuccess(
183 + provider: description,
184 + function: 'fetchRate',
185 + requestData: {
186 + 'from': from.title,
187 + 'to': to.title,
188 + 'amount': amount,
189 + 'isFixedRateMode': isFixedRateMode,
190 + 'isReceiveAmount': isReceiveAmount,
191 + 'params': params,
192 + 'url': uri.toString(),
193 + },
194 + responseData: {
195 + 'fromAmount': fromAmount,
196 + 'toAmount': toAmount,
197 + 'rate': rate,
198 + 'rateId': rateId,
199 + 'provider': _provider.first,
200 + 'statusCode': response.statusCode,
201 + 'responseJSON': responseJSON,
202 + },
203 + );
204 +
205 + return rate;
206 + } catch (e, s) {
207 + ExchangeProviderLogger.logError(
208 + provider: description,
209 + function: 'fetchRate',
210 + error: e,
211 + stackTrace: s,
212 + requestData: {
213 + 'from': from.title,
214 + 'to': to.title,
215 + 'amount': amount,
216 + 'isFixedRateMode': isFixedRateMode,
217 + 'isReceiveAmount': isReceiveAmount,
218 + },
219 + );
220 printV(e.toString());
221 return 0.0;
222 }
@@ -201,6 +255,23 @@ class TrocadorExchangeProvider extends ExchangeProvider {
255 }
256
257 if (_provider.isEmpty) {
258 + ExchangeProviderLogger.logError(
259 + provider: description,
260 + function: 'createTrade',
261 + error: Exception('No available provider is enabled'),
262 + stackTrace: StackTrace.current,
263 + requestData: {
264 + 'from': request.fromCurrency.title,
265 + 'to': request.toCurrency.title,
266 + 'fromAmount': request.fromAmount,
267 + 'toAmount': request.toAmount,
268 + 'toAddress': request.toAddress,
269 + 'refundAddress': request.refundAddress,
270 + 'isFixedRateMode': isFixedRateMode,
271 + 'isSendAll': isSendAll,
272 + 'params': params,
273 + },
274 + );
275 throw Exception('No available provider is enabled');
276 }
277
@@ -214,11 +285,50 @@ class TrocadorExchangeProvider extends ExchangeProvider {
285 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
286 final error = responseJSON['error'] as String;
287 final message = responseJSON['message'] as String;
288 +
289 + ExchangeProviderLogger.logError(
290 + provider: description,
291 + function: 'createTrade',
292 + error: Exception('${error}\n$message'),
293 + stackTrace: StackTrace.current,
294 + requestData: {
295 + 'from': request.fromCurrency.title,
296 + 'to': request.toCurrency.title,
297 + 'fromAmount': request.fromAmount,
298 + 'toAmount': request.toAmount,
299 + 'toAddress': request.toAddress,
300 + 'refundAddress': request.refundAddress,
301 + 'isFixedRateMode': isFixedRateMode,
302 + 'isSendAll': isSendAll,
303 + 'params': params,
304 + 'url': uri.toString(),
305 + },
306 + );
307 +
308 throw Exception('${error}\n$message');
309 }
310
220 - if (response.statusCode != 200)
311 + if (response.statusCode != 200) {
312 + ExchangeProviderLogger.logError(
313 + provider: description,
314 + function: 'createTrade',
315 + error: Exception('Unexpected http status: ${response.statusCode}'),
316 + stackTrace: StackTrace.current,
317 + requestData: {
318 + 'from': request.fromCurrency.title,
319 + 'to': request.toCurrency.title,
320 + 'fromAmount': request.fromAmount,
321 + 'toAmount': request.toAmount,
322 + 'toAddress': request.toAddress,
323 + 'refundAddress': request.refundAddress,
324 + 'isFixedRateMode': isFixedRateMode,
325 + 'isSendAll': isSendAll,
326 + 'params': params,
327 + 'url': uri.toString(),
328 + },
329 + );
330 throw Exception('Unexpected http status: ${response.statusCode}');
331 + }
332
333 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
334 final id = responseJSON['trade_id'] as String;
@@ -234,6 +344,39 @@ class TrocadorExchangeProvider extends ExchangeProvider {
344 final receiveAmount = responseJSON['amount_to']?.toString();
345 final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
346
347 + ExchangeProviderLogger.logSuccess(
348 + provider: description,
349 + function: 'createTrade',
350 + requestData: {
351 + 'from': request.fromCurrency.title,
352 + 'to': request.toCurrency.title,
353 + 'fromAmount': request.fromAmount,
354 + 'toAmount': request.toAmount,
355 + 'toAddress': request.toAddress,
356 + 'refundAddress': request.refundAddress,
357 + 'isFixedRateMode': isFixedRateMode,
358 + 'isSendAll': isSendAll,
359 + 'params': params,
360 + 'url': uri.toString(),
361 + },
362 + responseData: {
363 + 'id': id,
364 + 'inputAddress': inputAddress,
365 + 'refundAddress': refundAddress,
366 + 'status': status,
367 + 'payoutAddress': payoutAddress,
368 + 'date': date,
369 + 'password': password,
370 + 'providerId': providerId,
371 + 'providerName': providerName,
372 + 'amount': amount,
373 + 'receiveAmount': receiveAmount,
374 + 'addressProviderMemo': addressProviderMemo,
375 + 'statusCode': response.statusCode,
376 + 'responseJSON': responseJSON,
377 + },
378 + );
379 +
380 return Trade(
381 id: id,
382 from: request.fromCurrency,
lib/exchange/provider/xoswap_exchange_provider.dart
+138 -5
@@ -11,6 +11,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
11 import 'package:cw_core/crypto_currency.dart';
12 import 'package:cw_core/utils/print_verbose.dart';
13 import 'package:cw_core/utils/proxy_wrapper.dart';
14 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
15 class XOSwapExchangeProvider extends ExchangeProvider {
16 XOSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported));
17
@@ -150,8 +151,24 @@ class XOSwapExchangeProvider extends ExchangeProvider {
151 }) async {
152 try {
153 final rates = await getRatesForPair(from: from, to: to);
153 - if (rates.isEmpty) return 0;
154 + if (rates.isEmpty) {
155 + ExchangeProviderLogger.logError(
156 + provider: description,
157 + function: 'fetchRate',
158 + error: Exception('No rates found for $from to $to'),
159 + stackTrace: StackTrace.current,
160 + requestData: {
161 + 'from': from.title,
162 + 'to': to.title,
163 + 'amount': amount,
164 + 'isFixedRateMode': isFixedRateMode,
165 + 'isReceiveAmount': isReceiveAmount,
166 + },
167 + );
168 + return 0;
169 + }
170
171 + double result;
172 if (!isFixedRateMode) {
173 double bestOutput = 0.0;
174 for (var rate in rates) {
@@ -166,7 +183,7 @@ class XOSwapExchangeProvider extends ExchangeProvider {
183 }
184 }
185 }
169 - return bestOutput > 0 ? (bestOutput / amount) : 0;
186 + result = bestOutput > 0 ? (bestOutput / amount) : 0;
187 } else {
188 double bestInput = double.infinity;
189 for (var rate in rates) {
@@ -181,9 +198,41 @@ class XOSwapExchangeProvider extends ExchangeProvider {
198 }
199 }
200 }
184 - return bestInput < double.infinity ? amount / bestInput : 0;
201 + result = bestInput < double.infinity ? amount / bestInput : 0;
202 }
186 - } catch (e) {
203 +
204 + ExchangeProviderLogger.logSuccess(
205 + provider: description,
206 + function: 'fetchRate',
207 + requestData: {
208 + 'from': from.title,
209 + 'to': to.title,
210 + 'amount': amount,
211 + 'isFixedRateMode': isFixedRateMode,
212 + 'isReceiveAmount': isReceiveAmount,
213 + },
214 + responseData: {
215 + 'result': result,
216 + 'ratesCount': rates.length,
217 + 'rates': rates,
218 + },
219 + );
220 +
221 + return result;
222 + } catch (e, s) {
223 + ExchangeProviderLogger.logError(
224 + provider: description,
225 + function: 'fetchRate',
226 + error: e,
227 + stackTrace: s,
228 + requestData: {
229 + 'from': from.title,
230 + 'to': to.title,
231 + 'amount': amount,
232 + 'isFixedRateMode': isFixedRateMode,
233 + 'isReceiveAmount': isReceiveAmount,
234 + },
235 + );
236 printV(e.toString());
237 return 0;
238 }
@@ -202,6 +251,24 @@ class XOSwapExchangeProvider extends ExchangeProvider {
251 final curTo = await _getAssets(request.toCurrency);
252
253 if (curFrom == null || curTo == null) {
254 + ExchangeProviderLogger.logError(
255 + provider: description,
256 + function: 'createTrade',
257 + error: TradeNotCreatedException(description),
258 + stackTrace: StackTrace.current,
259 + requestData: {
260 + 'from': request.fromCurrency.title,
261 + 'to': request.toCurrency.title,
262 + 'fromAmount': request.fromAmount,
263 + 'toAmount': request.toAmount,
264 + 'toAddress': request.toAddress,
265 + 'refundAddress': request.refundAddress,
266 + 'isFixedRateMode': isFixedRateMode,
267 + 'isSendAll': isSendAll,
268 + 'curFrom': curFrom,
269 + 'curTo': curTo,
270 + },
271 + );
272 throw TradeNotCreatedException(description);
273 }
274
@@ -225,6 +292,26 @@ class XOSwapExchangeProvider extends ExchangeProvider {
292 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
293 final error = responseJSON['error'] ?? 'Unknown error';
294 final message = responseJSON['message'] ?? '';
295 +
296 + ExchangeProviderLogger.logError(
297 + provider: description,
298 + function: 'createTrade',
299 + error: Exception('$error\n$message'),
300 + stackTrace: StackTrace.current,
301 + requestData: {
302 + 'from': request.fromCurrency.title,
303 + 'to': request.toCurrency.title,
304 + 'fromAmount': request.fromAmount,
305 + 'toAmount': request.toAmount,
306 + 'toAddress': request.toAddress,
307 + 'refundAddress': request.refundAddress,
308 + 'isFixedRateMode': isFixedRateMode,
309 + 'isSendAll': isSendAll,
310 + 'payload': payload,
311 + 'url': uri.toString(),
312 + },
313 + );
314 +
315 throw Exception('$error\n$message');
316 }
317 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -245,6 +332,36 @@ class XOSwapExchangeProvider extends ExchangeProvider {
332
333 final createdAt = DateTime.parse(createdAtString).toLocal();
334
335 + ExchangeProviderLogger.logSuccess(
336 + provider: description,
337 + function: 'createTrade',
338 + requestData: {
339 + 'from': request.fromCurrency.title,
340 + 'to': request.toCurrency.title,
341 + 'fromAmount': request.fromAmount,
342 + 'toAmount': request.toAmount,
343 + 'toAddress': request.toAddress,
344 + 'refundAddress': request.refundAddress,
345 + 'isFixedRateMode': isFixedRateMode,
346 + 'isSendAll': isSendAll,
347 + 'payload': payload,
348 + 'url': uri.toString(),
349 + },
350 + responseData: {
351 + 'orderId': orderId,
352 + 'depositAddress': depositAddress,
353 + 'payoutAddress': payoutAddress,
354 + 'refundAddress': refundAddress,
355 + 'depositAmount': depositAmount,
356 + 'receiveAmount': receiveAmount,
357 + 'status': status,
358 + 'createdAt': createdAtString,
359 + 'extraId': extraId,
360 + 'statusCode': response.statusCode,
361 + 'responseJSON': responseJSON,
362 + },
363 + );
364 +
365 return Trade(
366 id: orderId,
367 from: from,
@@ -262,7 +379,23 @@ class XOSwapExchangeProvider extends ExchangeProvider {
379 userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
380 isSendAll: isSendAll,
381 );
265 - } catch (e) {
382 + } catch (e, s) {
383 + ExchangeProviderLogger.logError(
384 + provider: description,
385 + function: 'createTrade',
386 + error: e,
387 + stackTrace: s,
388 + requestData: {
389 + 'from': request.fromCurrency.title,
390 + 'to': request.toCurrency.title,
391 + 'fromAmount': request.fromAmount,
392 + 'toAmount': request.toAmount,
393 + 'toAddress': request.toAddress,
394 + 'refundAddress': request.refundAddress,
395 + 'isFixedRateMode': isFixedRateMode,
396 + 'isSendAll': isSendAll,
397 + },
398 + );
399 printV(e.toString());
400 throw TradeNotCreatedException(description);
401 }
lib/router.dart
+6
@@ -34,6 +34,7 @@ import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart';
34 import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
35 import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
36 import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
37 +import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart';
38 import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
39 import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
40 import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
@@ -924,6 +925,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
925 builder: (_) => getIt.get<DevNetworkRequests>(),
926 );
927
928 + case Routes.devExchangeProviderLogs:
929 + return MaterialPageRoute<void>(
930 + builder: (_) => getIt.get<DevExchangeProviderLogsPage>(),
931 + );
932 +
933 case Routes.devMoneroCallProfiler:
934 return MaterialPageRoute<void>(
935 builder: (_) => getIt.get<DevMoneroCallProfilerPage>(),
lib/routes.dart
+1
@@ -123,6 +123,7 @@ class Routes {
123 static const devBackgroundSyncLogs = '/dev/background_sync_logs';
124 static const devSocketHealthLogs = '/dev/socket_health_logs';
125 static const devNetworkRequests = '/dev/network_requests';
126 + static const devExchangeProviderLogs = '/dev/exchange_provider_logs';
127
128 static const signPage = '/sign_page';
129 static const connectDevices = '/device/connect';
lib/src/screens/dev/exchange_provider_logs_page.dart new
+562
@@ -0,0 +1,562 @@
1 +import 'package:cake_wallet/src/screens/base_page.dart';
2 +import 'package:cake_wallet/view_model/dev/exchange_provider_logs_view_model.dart';
3 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
4 +import 'package:flutter/material.dart';
5 +import 'package:flutter/services.dart';
6 +import 'package:flutter_mobx/flutter_mobx.dart';
7 +import 'package:intl/intl.dart';
8 +
9 +class DevExchangeProviderLogsPage extends BasePage {
10 + final ExchangeProviderLogsViewModel viewModel;
11 +
12 + DevExchangeProviderLogsPage(this.viewModel) {
13 + viewModel.loadLogs();
14 + }
15 +
16 + @override
17 + String? get title => "[dev] exchange provider logs";
18 +
19 + @override
20 + Widget? trailing(BuildContext context) {
21 + return IconButton(
22 + icon: Icon(Icons.refresh),
23 + onPressed: () => viewModel.refreshLogs(),
24 + );
25 + }
26 +
27 + @override
28 + Widget body(BuildContext context) {
29 + return Observer(
30 + builder: (_) {
31 + if (viewModel.isLoading) {
32 + return Center(child: CircularProgressIndicator());
33 + }
34 +
35 + if (viewModel.error != null) {
36 + return Center(child: Text("Error: ${viewModel.error}"));
37 + }
38 +
39 + if (viewModel.logs.isEmpty) {
40 + return Center(
41 + child: Column(
42 + mainAxisAlignment: MainAxisAlignment.center,
43 + children: [
44 + Text("No exchange provider logs available"),
45 + SizedBox(height: 16),
46 + ElevatedButton(
47 + onPressed: () => viewModel.loadLogs(),
48 + child: Text("Load Logs"),
49 + ),
50 + ],
51 + ),
52 + );
53 + }
54 +
55 + return Column(
56 + crossAxisAlignment: CrossAxisAlignment.start,
57 + children: [
58 + _StatsCard(viewModel),
59 + _LogsHeader(viewModel),
60 + Expanded(
61 + child: _LogsList(viewModel),
62 + ),
63 + _ActionButtons(viewModel),
64 + ],
65 + );
66 + },
67 + );
68 + }
69 +}
70 +
71 +class _StatsCard extends StatefulWidget {
72 + final ExchangeProviderLogsViewModel viewModel;
73 +
74 + const _StatsCard(this.viewModel);
75 +
76 + @override
77 + State<_StatsCard> createState() => _StatsCardState();
78 +}
79 +
80 +class _StatsCardState extends State<_StatsCard> {
81 + bool _isExpanded = false;
82 +
83 + @override
84 + Widget build(BuildContext context) {
85 + return Card(
86 + margin: EdgeInsets.all(16),
87 + child: Column(
88 + children: [
89 + ListTile(
90 + title: Text(
91 + "Stats",
92 + style: Theme.of(context).textTheme.titleLarge?.copyWith(
93 + fontSize: 18,
94 + ),
95 + ),
96 + trailing: Icon(
97 + _isExpanded ? Icons.expand_less : Icons.expand_more,
98 + ),
99 + onTap: () {
100 + setState(() {
101 + _isExpanded = !_isExpanded;
102 + });
103 + },
104 + ),
105 + if (_isExpanded) ...[
106 + Observer(
107 + builder: (_) => Padding(
108 + padding: EdgeInsets.fromLTRB(16, 0, 16, 16),
109 + child: Column(
110 + crossAxisAlignment: CrossAxisAlignment.start,
111 + children: [
112 + Row(
113 + mainAxisAlignment: MainAxisAlignment.spaceAround,
114 + children: [
115 + _StatItem("Total", widget.viewModel.totalLogs.toString()),
116 + _StatItem("Success", widget.viewModel.successLogs.toString()),
117 + _StatItem("Errors", widget.viewModel.errorLogs.toString()),
118 + ],
119 + ),
120 + SizedBox(height: 16),
121 + Text(
122 + "Logs by Provider:",
123 + style: Theme.of(context).textTheme.titleMedium,
124 + ),
125 + SizedBox(height: 8),
126 + ...widget.viewModel.logsByProvider.entries.map(
127 + (entry) => Padding(
128 + padding: EdgeInsets.symmetric(vertical: 2),
129 + child: Row(
130 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
131 + children: [
132 + Text(entry.key.title),
133 + Text(entry.value.toString()),
134 + ],
135 + ),
136 + ),
137 + ),
138 + ],
139 + ),
140 + ),
141 + ),
142 + ],
143 + ],
144 + ),
145 + );
146 + }
147 +}
148 +
149 +class _LogsHeader extends StatelessWidget {
150 + final ExchangeProviderLogsViewModel viewModel;
151 +
152 + const _LogsHeader(this.viewModel);
153 +
154 + @override
155 + Widget build(BuildContext context) {
156 + return Padding(
157 + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
158 + child: Row(
159 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
160 + children: [
161 + Text(
162 + "Logs",
163 + style: Theme.of(context).textTheme.titleMedium,
164 + ),
165 + Observer(
166 + builder: (_) => PopupMenuButton<LogFilter>(
167 + tooltip: "Filter logs",
168 + onSelected: (LogFilter filter) {
169 + viewModel.setFilter(filter);
170 + },
171 + itemBuilder: (context) => [
172 + PopupMenuItem(
173 + value: LogFilter.all,
174 + child: Row(
175 + children: [
176 + Icon(Icons.list, size: 18),
177 + SizedBox(width: 8),
178 + Text("All Logs"),
179 + ],
180 + ),
181 + ),
182 + PopupMenuItem(
183 + value: LogFilter.success,
184 + child: Row(
185 + children: [
186 + Icon(Icons.check_circle, size: 18, color: Colors.green),
187 + SizedBox(width: 8),
188 + Text("Success Only"),
189 + ],
190 + ),
191 + ),
192 + PopupMenuItem(
193 + value: LogFilter.error,
194 + child: Row(
195 + children: [
196 + Icon(Icons.error, size: 18, color: Colors.red),
197 + SizedBox(width: 8),
198 + Text("Errors Only"),
199 + ],
200 + ),
201 + ),
202 + ],
203 + child: Container(
204 + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
205 + decoration: BoxDecoration(
206 + border: Border.all(color: Theme.of(context).dividerColor),
207 + borderRadius: BorderRadius.circular(4),
208 + ),
209 + child: Row(
210 + mainAxisSize: MainAxisSize.min,
211 + children: [
212 + Icon(Icons.filter_list, size: 16),
213 + SizedBox(width: 4),
214 + Text(_getFilterText(viewModel.currentFilter)),
215 + Icon(Icons.arrow_drop_down, size: 16),
216 + ],
217 + ),
218 + ),
219 + ),
220 + ),
221 + ],
222 + ),
223 + );
224 + }
225 +
226 + String _getFilterText(LogFilter filter) {
227 + switch (filter) {
228 + case LogFilter.all:
229 + return "All";
230 + case LogFilter.success:
231 + return "Success";
232 + case LogFilter.error:
233 + return "Errors";
234 + }
235 + }
236 +}
237 +
238 +class _StatItem extends StatelessWidget {
239 + final String label;
240 + final String value;
241 +
242 + const _StatItem(this.label, this.value);
243 +
244 + @override
245 + Widget build(BuildContext context) {
246 + return Column(
247 + children: [
248 + Text(
249 + value,
250 + style: Theme.of(context).textTheme.headlineSmall?.copyWith(
251 + fontWeight: FontWeight.bold,
252 + ),
253 + ),
254 + Text(label),
255 + ],
256 + );
257 + }
258 +}
259 +
260 +class _LogsList extends StatelessWidget {
261 + final ExchangeProviderLogsViewModel viewModel;
262 +
263 + const _LogsList(this.viewModel);
264 +
265 + @override
266 + Widget build(BuildContext context) {
267 + return Observer(
268 + builder: (_) => ListView.builder(
269 + itemCount: viewModel.filteredLogs.length,
270 + itemBuilder: (context, index) {
271 + final log = viewModel.filteredLogs[index];
272 + return _LogEntryCard(log);
273 + },
274 + ),
275 + );
276 + }
277 +}
278 +
279 +class _LogEntryCard extends StatelessWidget {
280 + final ExchangeProviderLogEntry log;
281 +
282 + const _LogEntryCard(this.log);
283 +
284 + void _copyToClipboard(BuildContext context, String text) {
285 + Clipboard.setData(ClipboardData(text: text));
286 + ScaffoldMessenger.of(context).showSnackBar(
287 + SnackBar(
288 + content: Text("Copied to clipboard"),
289 + duration: Duration(seconds: 2),
290 + ),
291 + );
292 + }
293 +
294 + @override
295 + Widget build(BuildContext context) {
296 + final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
297 +
298 + return Card(
299 + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
300 + child: ExpansionTile(
301 + title: Row(
302 + children: [
303 + Icon(
304 + log.isSuccess ? Icons.check_circle : Icons.error,
305 + color: log.isSuccess ? Colors.green : Colors.red,
306 + size: 20,
307 + ),
308 + SizedBox(width: 8),
309 + Expanded(
310 + child: Text(
311 + "${log.provider.title} - ${log.function}",
312 + style: Theme.of(context).textTheme.titleMedium,
313 + ),
314 + ),
315 + ],
316 + ),
317 + subtitle: Text(
318 + dateFormat.format(log.timestamp),
319 + style: Theme.of(context).textTheme.bodySmall,
320 + ),
321 + children: [
322 + Padding(
323 + padding: EdgeInsets.all(16),
324 + child: Column(
325 + crossAxisAlignment: CrossAxisAlignment.start,
326 + children: [
327 + if (log.error != null) ...[
328 + Text(
329 + "Error:",
330 + style: Theme.of(context).textTheme.titleSmall?.copyWith(
331 + color: Colors.red,
332 + ),
333 + ),
334 + SizedBox(height: 4),
335 + Container(
336 + width: double.infinity,
337 + padding: EdgeInsets.all(8),
338 + decoration: BoxDecoration(
339 + color: Colors.red.shade50,
340 + borderRadius: BorderRadius.circular(4),
341 + border: Border.all(color: Colors.red.shade200),
342 + ),
343 + child: GestureDetector(
344 + onTap: () => _copyToClipboard(context, log.error!),
345 + child: Text(
346 + log.error!,
347 + style: TextStyle(
348 + fontFamily: 'monospace',
349 + fontSize: 12,
350 + color: Colors.black,
351 + ),
352 + ),
353 + ),
354 + ),
355 + SizedBox(height: 16),
356 + ],
357 + if (log.stackTrace != null) ...[
358 + Text(
359 + "Stack Trace:",
360 + style: Theme.of(context).textTheme.titleSmall,
361 + ),
362 + SizedBox(height: 4),
363 + Container(
364 + width: double.infinity,
365 + padding: EdgeInsets.all(8),
366 + decoration: BoxDecoration(
367 + color: Colors.grey.shade100,
368 + borderRadius: BorderRadius.circular(4),
369 + border: Border.all(color: Colors.grey.shade300),
370 + ),
371 + child: GestureDetector(
372 + onTap: () => _copyToClipboard(context, log.stackTrace!),
373 + child: Text(
374 + log.stackTrace!,
375 + style: TextStyle(
376 + fontFamily: 'monospace',
377 + fontSize: 10,
378 + color: Colors.black,
379 + ),
380 + ),
381 + ),
382 + ),
383 + SizedBox(height: 16),
384 + ],
385 + if (log.callStack != null) ...[
386 + Text(
387 + "Call Stack:",
388 + style: Theme.of(context).textTheme.titleSmall,
389 + ),
390 + SizedBox(height: 4),
391 + Container(
392 + width: double.infinity,
393 + padding: EdgeInsets.all(8),
394 + decoration: BoxDecoration(
395 + color: Colors.blue.shade50,
396 + borderRadius: BorderRadius.circular(4),
397 + border: Border.all(color: Colors.blue.shade200),
398 + ),
399 + child: GestureDetector(
400 + onTap: () => _copyToClipboard(context, log.callStack!),
401 + child: Text(
402 + log.callStack!,
403 + style: TextStyle(
404 + fontFamily: 'monospace',
405 + fontSize: 10,
406 + color: Colors.black,
407 + ),
408 + ),
409 + ),
410 + ),
411 + SizedBox(height: 16),
412 + ],
413 + if (log.requestData != null) ...[
414 + Text(
415 + "Request Data:",
416 + style: Theme.of(context).textTheme.titleSmall,
417 + ),
418 + SizedBox(height: 4),
419 + Container(
420 + width: double.infinity,
421 + padding: EdgeInsets.all(8),
422 + decoration: BoxDecoration(
423 + color: Colors.green.shade50,
424 + borderRadius: BorderRadius.circular(4),
425 + border: Border.all(color: Colors.green.shade200),
426 + ),
427 + child: GestureDetector(
428 + onTap: () => _copyToClipboard(context, log.requestData.toString()),
429 + child: Text(
430 + log.requestData.toString(),
431 + style: TextStyle(
432 + fontFamily: 'monospace',
433 + fontSize: 10,
434 + color: Colors.black,
435 + ),
436 + ),
437 + ),
438 + ),
439 + SizedBox(height: 16),
440 + ],
441 + if (log.responseData != null) ...[
442 + Text(
443 + "Response Data:",
444 + style: Theme.of(context).textTheme.titleSmall,
445 + ),
446 + SizedBox(height: 4),
447 + Container(
448 + width: double.infinity,
449 + padding: EdgeInsets.all(8),
450 + decoration: BoxDecoration(
451 + color: Colors.orange.shade50,
452 + borderRadius: BorderRadius.circular(4),
453 + border: Border.all(color: Colors.orange.shade200),
454 + ),
455 + child: GestureDetector(
456 + onTap: () => _copyToClipboard(context, log.responseData.toString()),
457 + child: Text(
458 + log.responseData.toString(),
459 + style: TextStyle(
460 + fontFamily: 'monospace',
461 + fontSize: 10,
462 + color: Colors.black,
463 + ),
464 + ),
465 + ),
466 + ),
467 + ],
468 + ],
469 + ),
470 + ),
471 + ],
472 + ),
473 + );
474 + }
475 +}
476 +
477 +class _ActionButtons extends StatelessWidget {
478 + final ExchangeProviderLogsViewModel viewModel;
479 +
480 + const _ActionButtons(this.viewModel);
481 +
482 + @override
483 + Widget build(BuildContext context) {
484 + return Padding(
485 + padding: EdgeInsets.all(16),
486 + child: Wrap(
487 + spacing: 8,
488 + runSpacing: 8,
489 + alignment: WrapAlignment.center,
490 + runAlignment: WrapAlignment.center,
491 + children: [
492 + ElevatedButton.icon(
493 + onPressed: () => _copyLogsAsText(context),
494 + icon: Icon(Icons.copy, size: 18),
495 + label: Text("Copy Text"),
496 + style: ElevatedButton.styleFrom(
497 + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
498 + ),
499 + ),
500 + ElevatedButton.icon(
501 + onPressed: () => _copyLogsAsJson(context),
502 + icon: Icon(Icons.code, size: 18),
503 + label: Text("Copy JSON"),
504 + style: ElevatedButton.styleFrom(
505 + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
506 + ),
507 + ),
508 + ElevatedButton.icon(
509 + onPressed: () => _showClearDialog(context),
510 + icon: Icon(Icons.clear, size: 18),
511 + label: Text("Clear"),
512 + style: ElevatedButton.styleFrom(
513 + backgroundColor: Colors.red,
514 + foregroundColor: Colors.white,
515 + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
516 + ),
517 + ),
518 + ],
519 + ),
520 + );
521 + }
522 +
523 + void _copyLogsAsText(BuildContext context) {
524 + final logsText = viewModel.getLogsAsText();
525 + Clipboard.setData(ClipboardData(text: logsText));
526 + ScaffoldMessenger.of(context).showSnackBar(
527 + SnackBar(content: Text("Logs copied to clipboard")),
528 + );
529 + }
530 +
531 + void _copyLogsAsJson(BuildContext context) {
532 + final logsJson = viewModel.getLogsAsJson();
533 + Clipboard.setData(ClipboardData(text: logsJson));
534 + ScaffoldMessenger.of(context).showSnackBar(
535 + SnackBar(content: Text("Logs JSON copied to clipboard")),
536 + );
537 + }
538 +
539 + void _showClearDialog(BuildContext context) {
540 + showDialog(
541 + context: context,
542 + builder: (context) => AlertDialog(
543 + title: Text("Clear Exchange Provider Logs"),
544 + content: Text(
545 + "Are you sure you want to clear all exchange provider logs? This action cannot be undone."),
546 + actions: [
547 + TextButton(
548 + onPressed: () => Navigator.of(context).pop(),
549 + child: Text("Cancel"),
550 + ),
551 + TextButton(
552 + onPressed: () {
553 + viewModel.clearLogs();
554 + Navigator.of(context).pop();
555 + },
556 + child: Text("Clear", style: TextStyle(color: Colors.red)),
557 + ),
558 + ],
559 + ),
560 + );
561 + }
562 +}
lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart
+4 -2
@@ -73,7 +73,8 @@ class ExchangeTradeExternalSendPage extends BasePage {
73 Routes.fullscreenQR,
74 arguments: QrViewData(
75 embeddedImagePath: exchangeTradeViewModel.qrImage,
76 - data: exchangeTradeViewModel.trade.inputAddress ??
76 + data: exchangeTradeViewModel.paymentUri?.toString() ??
77 + exchangeTradeViewModel.trade.inputAddress ??
78 fetchingLabel,
79 ),
80 );
@@ -92,7 +93,8 @@ class ExchangeTradeExternalSendPage extends BasePage {
93 ),
94 ),
95 child: QrImage(
95 - data: exchangeTradeViewModel.trade.inputAddress ??
96 + data: exchangeTradeViewModel.paymentUri?.toString() ??
97 + exchangeTradeViewModel.trade.inputAddress ??
98 fetchingLabel,
99 embeddedImagePath: exchangeTradeViewModel.qrImage,
100 size: 230,
lib/src/screens/receive/widgets/qr_widget.dart
+1
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/core/payment_uris.dart';
2 import 'package:cake_wallet/entities/qr_view_data.dart';
3 import 'package:cake_wallet/src/widgets/primary_button.dart';
4 import 'package:cake_wallet/routes.dart';
lib/src/screens/send/widgets/send_card.dart
+8 -4
@@ -121,8 +121,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
121 WidgetsBinding.instance.addPostFrameCallback(
122 (timeStamp) {
123 if (mounted) {
124 - final separator = initialPaymentRequest!.scheme.isNotEmpty ? ":" : "";
125 - final uri = initialPaymentRequest!.scheme + separator + initialPaymentRequest!.address;
124 + final prefix = initialPaymentRequest!.scheme.isNotEmpty ? "${initialPaymentRequest!.scheme}:" : "";
125 + final amount = initialPaymentRequest!.amount.isNotEmpty ? "?amount=${initialPaymentRequest!.amount}" : "";
126 + final uri = prefix + initialPaymentRequest!.address + amount;
127 _handlePaymentFlow(uri, initialPaymentRequest!);
128 }
129 },
@@ -145,6 +146,10 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
146 try {
147 final result = await paymentViewModel.processAddress(uri);
148
149 + if (paymentRequest.contractAddress != null) {
150 + await sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!);
151 + }
152 +
153 switch (result.type) {
154 case PaymentFlowType.singleWallet:
155 case PaymentFlowType.multipleWallets:
@@ -256,8 +261,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
261 }
262 });
263 await Future.delayed(const Duration(seconds: 2));
259 - if (loadingBottomSheetContext != null &&
260 - loadingBottomSheetContext!.mounted) {
264 + if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
265 Navigator.of(loadingBottomSheetContext!).pop();
266 }
267 _applyPaymentRequest(paymentRequest);
lib/src/screens/settings/other_settings_page.dart
+6
@@ -111,6 +111,12 @@ class OtherSettingsPage extends BasePage {
111 handler: (BuildContext context) =>
112 Navigator.of(context).pushNamed(Routes.devNetworkRequests),
113 ),
114 + if (FeatureFlag.hasDevOptions)
115 + SettingsCellWithArrow(
116 + title: '[dev] exchange provider logs',
117 + handler: (BuildContext context) =>
118 + Navigator.of(context).pushNamed(Routes.devExchangeProviderLogs),
119 + ),
120 Spacer(),
121 SettingsVersionCell(
122 title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
lib/utils/exchange_provider_logger.dart new
+189
@@ -0,0 +1,189 @@
1 +import 'dart:convert';
2 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
3 +
4 +class ExchangeProviderLogEntry {
5 + final DateTime timestamp;
6 + final ExchangeProviderDescription provider;
7 + final String function;
8 + final String? error;
9 + final String? stackTrace;
10 + final String? callStack;
11 + final Map<String, dynamic>? requestData;
12 + final Map<String, dynamic>? responseData;
13 + final bool isSuccess;
14 +
15 + ExchangeProviderLogEntry({
16 + required this.timestamp,
17 + required this.provider,
18 + required this.function,
19 + this.error,
20 + this.stackTrace,
21 + this.callStack,
22 + this.requestData,
23 + this.responseData,
24 + required this.isSuccess,
25 + });
26 +
27 + String toLogString() {
28 + final buffer = StringBuffer();
29 + buffer.writeln('Provider: ${provider.title}');
30 + buffer.writeln('Function: $function');
31 + buffer.writeln('Timestamp: ${timestamp.toIso8601String()}');
32 + buffer.writeln('Success: $isSuccess');
33 +
34 + if (error != null) {
35 + buffer.writeln('Error: $error');
36 + }
37 +
38 + if (stackTrace != null) {
39 + buffer.writeln('StackTrace: $stackTrace');
40 + }
41 +
42 + if (callStack != null) {
43 + buffer.writeln('CallStack: $callStack');
44 + }
45 +
46 + if (requestData != null) {
47 + buffer.writeln('Request: ${json.encode(requestData)}');
48 + }
49 +
50 + if (responseData != null) {
51 + buffer.writeln('Response: ${json.encode(responseData)}');
52 + }
53 +
54 + buffer.writeln('---');
55 + return buffer.toString();
56 + }
57 +
58 + Map<String, dynamic> toJson() {
59 + return {
60 + 'timestamp': timestamp.toIso8601String(),
61 + 'provider': provider.title,
62 + 'function': function,
63 + 'error': error,
64 + 'stackTrace': stackTrace,
65 + 'callStack': callStack,
66 + 'requestData': requestData,
67 + 'responseData': responseData,
68 + 'isSuccess': isSuccess,
69 + };
70 + }
71 +
72 + factory ExchangeProviderLogEntry.fromJson(Map<String, dynamic> json) {
73 + final allProviders = [
74 + ExchangeProviderDescription.xmrto,
75 + ExchangeProviderDescription.changeNow,
76 + ExchangeProviderDescription.morphToken,
77 + ExchangeProviderDescription.sideShift,
78 + ExchangeProviderDescription.simpleSwap,
79 + ExchangeProviderDescription.trocador,
80 + ExchangeProviderDescription.exolix,
81 + ExchangeProviderDescription.all,
82 + ExchangeProviderDescription.thorChain,
83 + ExchangeProviderDescription.swapTrade,
84 + ExchangeProviderDescription.letsExchange,
85 + ExchangeProviderDescription.stealthEx,
86 + ExchangeProviderDescription.chainflip,
87 + ExchangeProviderDescription.xoSwap,
88 + ];
89 +
90 + return ExchangeProviderLogEntry(
91 + timestamp: DateTime.parse(json['timestamp'] as String),
92 + provider: allProviders.firstWhere(
93 + (p) => p.title == json['provider'] as String,
94 + orElse: () => ExchangeProviderDescription.changeNow,
95 + ),
96 + function: json['function'] as String,
97 + error: json['error'] as String?,
98 + stackTrace: json['stackTrace'] as String?,
99 + callStack: json['callStack'] as String?,
100 + requestData: json['requestData'] as Map<String, dynamic>?,
101 + responseData: json['responseData'] as Map<String, dynamic>?,
102 + isSuccess: json['isSuccess'] as bool,
103 + );
104 + }
105 +}
106 +
107 +class ExchangeProviderLogger {
108 + static final List<ExchangeProviderLogEntry> _logs = [];
109 + static const int maxLogs = 100;
110 +
111 + static List<ExchangeProviderLogEntry> get logs => List.unmodifiable(_logs);
112 +
113 + static void logSuccess({
114 + required ExchangeProviderDescription provider,
115 + required String function,
116 + Map<String, dynamic>? requestData,
117 + Map<String, dynamic>? responseData,
118 + String? callStack,
119 + }) {
120 + final entry = ExchangeProviderLogEntry(
121 + timestamp: DateTime.now(),
122 + provider: provider,
123 + function: function,
124 + requestData: requestData,
125 + responseData: responseData,
126 + callStack: callStack,
127 + isSuccess: true,
128 + );
129 +
130 + _addLog(entry);
131 + }
132 +
133 + static void logError({
134 + required ExchangeProviderDescription provider,
135 + required String function,
136 + required dynamic error,
137 + StackTrace? stackTrace,
138 + Map<String, dynamic>? requestData,
139 + String? callStack,
140 + }) {
141 + final entry = ExchangeProviderLogEntry(
142 + timestamp: DateTime.now(),
143 + provider: provider,
144 + function: function,
145 + error: error.toString(),
146 + stackTrace: stackTrace?.toString(),
147 + requestData: requestData,
148 + callStack: callStack,
149 + isSuccess: false,
150 + );
151 +
152 + _addLog(entry);
153 + }
154 +
155 + static void _addLog(ExchangeProviderLogEntry entry) {
156 + _logs.insert(0, entry);
157 +
158 + if (_logs.length > maxLogs * 2) {
159 + final excessCount = _logs.length - maxLogs;
160 + _logs.removeRange(0, excessCount);
161 + }
162 + }
163 +
164 + static void clearLogs() {
165 + _logs.clear();
166 + }
167 +
168 + static String getLogsAsText() {
169 + if (_logs.isEmpty) return 'No exchange provider logs available';
170 +
171 + final buffer = StringBuffer();
172 + buffer.writeln('Exchange Provider Logs');
173 + buffer.writeln('Generated: ${DateTime.now().toIso8601String()}');
174 + buffer.writeln('Total logs: ${_logs.length}');
175 + buffer.writeln('Success logs: ${_logs.where((log) => log.isSuccess).length}');
176 + buffer.writeln('Error logs: ${_logs.where((log) => !log.isSuccess).length}');
177 + buffer.writeln('');
178 +
179 + for (final log in _logs) {
180 + buffer.writeln(log.toLogString());
181 + }
182 +
183 + return buffer.toString();
184 + }
185 +
186 + static String getLogsAsJson() {
187 + return json.encode(_logs.map((log) => log.toJson()).toList());
188 + }
189 +}
lib/utils/payment_request.dart
+5 -55
@@ -1,10 +1,9 @@
1 +import 'package:cake_wallet/core/payment_uris.dart';
2 import 'package:cake_wallet/nano/nano.dart';
2 -import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
3 -import 'package:cw_core/format_fixed.dart';
3
4 class PaymentRequest {
5 PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
7 - {this.callbackUrl, this.callbackMessage});
6 + {this.callbackUrl, this.callbackMessage, this.contractAddress});
7
8 factory PaymentRequest.fromUri(Uri? uri) {
9 var address = "";
@@ -15,6 +14,7 @@ class PaymentRequest {
14 String? callbackUrl;
15 String? callbackMessage;
16 String? pjUri;
17 + String? contractAddress;
18
19 if (uri != null) {
20 if (uri.queryParameters['pj'] != null) {
@@ -34,6 +34,7 @@ class PaymentRequest {
34
35 address = paymentUri.address;
36 amount = paymentUri.amount;
37 + contractAddress = paymentUri.contractAddress;
38 }
39 }
40
@@ -41,8 +42,6 @@ class PaymentRequest {
42 scheme = walletType ?? "nano";
43 }
44
44 -
45 -
45 if (nano != null) {
46 if (amount.isNotEmpty) {
47 if (address.contains("nano")) {
@@ -61,6 +60,7 @@ class PaymentRequest {
60 pjUri,
61 callbackUrl: callbackUrl,
62 callbackMessage: callbackMessage,
63 + contractAddress: contractAddress,
64 );
65 }
66
@@ -71,55 +71,5 @@ class PaymentRequest {
71 final String? pjUri;
72 final String? callbackUrl;
73 final String? callbackMessage;
74 -}
75 -
76 -class ERC681URI extends PaymentURI {
77 - final int chainId;
74 final String? contractAddress;
79 -
80 - ERC681URI({
81 - required this.chainId,
82 - required super.address,
83 - required super.amount,
84 - required this.contractAddress,
85 - });
86 -
87 - factory ERC681URI.fromUri(Uri uri) {
88 - final (isContract, targetAddress) = _getTargetAddress(uri.path);
89 - final chainId = _getChainID(uri.path);
90 -
91 - final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
92 - final amount = isContract
93 - ? uri.queryParameters["uint256"]
94 - : uri.queryParameters["value"];
95 -
96 - var formatedAmount = "";
97 -
98 - if (amount != null) {
99 - formatedAmount = formatFixed(BigInt.parse(amount), 18);
100 - } else {
101 - formatedAmount = uri.queryParameters["amount"] ?? "";
102 - }
103 -
104 - return ERC681URI(
105 - chainId: chainId,
106 - address: address,
107 - amount: formatedAmount,
108 - contractAddress: isContract ? targetAddress : null,
109 - );
110 - }
111 -
112 - static int _getChainID(String path) {
113 - return int.parse(RegExp(
114 - r'@\d*',
115 - ).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
116 - "1");
117 - }
118 -
119 - static (bool, String) _getTargetAddress(String path) {
120 - final targetAddress = RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false)
121 - .firstMatch(path)!
122 - .group(0)!;
123 - return (path.contains("/"), targetAddress);
124 - }
75 }
lib/utils/token_utilities.dart new
+226
@@ -0,0 +1,226 @@
1 +import 'package:cw_core/cake_hive.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 +import 'package:cw_core/erc20_token.dart';
4 +import 'package:cw_core/spl_token.dart';
5 +import 'package:cw_core/tron_token.dart';
6 +import 'package:cw_core/wallet_base.dart';
7 +import 'package:cw_core/wallet_info.dart';
8 +import 'package:cw_core/wallet_type.dart';
9 +import 'package:hive/hive.dart';
10 +
11 +class TokenUtilities {
12 + static Future<List<Erc20Token>> loadAllUniqueEvmTokens(
13 + Box<WalletInfo> walletInfoSource,
14 + ) async {
15 + final evmWallets = walletInfoSource.values.where(
16 + (w) => w.type == WalletType.ethereum || w.type == WalletType.polygon,
17 + );
18 +
19 + final seen = <String>{};
20 + final unique = <Erc20Token>[];
21 +
22 + for (final wallet in evmWallets) {
23 + final chain = wallet.type == WalletType.ethereum ? 'ETH' : 'POL';
24 + final box = await _openEvmTokensBoxFor(wallet);
25 +
26 + for (final t in box.values.where((t) => t.enabled)) {
27 + final key = '$chain|${t.contractAddress.toLowerCase()}';
28 + if (seen.add(key)) {
29 + unique.add(t);
30 + }
31 + }
32 + }
33 +
34 + return unique;
35 + }
36 +
37 + static Future<List<SPLToken>> loadAllUniqueSolTokens(
38 + Box<WalletInfo> walletInfoSource,
39 + ) async {
40 + final solWallets = walletInfoSource.values.where(
41 + (w) => w.type == WalletType.solana,
42 + );
43 +
44 + final tokens = <SPLToken>[];
45 + for (final wallet in solWallets) {
46 + final box = await _openSolTokensBoxFor(wallet);
47 + tokens.addAll(box.values.where((t) => t.enabled));
48 + }
49 +
50 + final seen = <String>{};
51 + final unique = <SPLToken>[];
52 + for (final token in tokens) {
53 + final key = token.mintAddress.toLowerCase();
54 + if (seen.add(key)) unique.add(token);
55 + }
56 + return unique;
57 + }
58 +
59 + static Future<List<TronToken>> loadAllUniqueTronTokens(
60 + Box<WalletInfo> walletInfoSource,
61 + ) async {
62 + final tronWallets = walletInfoSource.values.where(
63 + (w) => w.type == WalletType.tron,
64 + );
65 +
66 + final seen = <String>{};
67 + final unique = <TronToken>[];
68 + for (final wallet in tronWallets) {
69 + final box = await _openTronTokensBoxFor(wallet);
70 + for (final t in box.values.where((t) => t.enabled)) {
71 + final key = t.contractAddress.toLowerCase();
72 + if (seen.add(key)) unique.add(t);
73 + }
74 + }
75 + return unique;
76 + }
77 +
78 + /// Finds a token by address across wallets depending on [walletType]
79 + /// - EVM chains: match by contractAddress
80 + /// - Solana: match by mintAddress
81 + /// - Tron: match by contractAddress
82 + static Future<CryptoCurrency?> findTokenByAddress({
83 + required WalletType walletType,
84 + required Box<WalletInfo> walletInfoSource,
85 + required String address,
86 + }) async {
87 + final lower = address.toLowerCase();
88 + switch (walletType) {
89 + case WalletType.ethereum:
90 + case WalletType.polygon:
91 + final tokens = await loadAllUniqueEvmTokens(walletInfoSource);
92 + for (final t in tokens) {
93 + if (t.contractAddress.toLowerCase() == lower) return t;
94 + }
95 + return null;
96 + case WalletType.solana:
97 + final solTokens = await loadAllUniqueSolTokens(walletInfoSource);
98 + for (final t in solTokens) {
99 + if (t.mintAddress.toLowerCase() == lower) return t;
100 + }
101 + return null;
102 + case WalletType.tron:
103 + final tronTokens = await loadAllUniqueTronTokens(walletInfoSource);
104 + for (final t in tronTokens) {
105 + if (t.contractAddress.toLowerCase() == lower) return t;
106 + }
107 + return null;
108 + default:
109 + return null;
110 + }
111 + }
112 +
113 + static Future<Box<Erc20Token>> _openEvmTokensBoxFor(
114 + WalletInfo walletInfo,
115 + ) async {
116 + final walletKey = walletInfo.name.replaceAll(' ', '_');
117 + final boxName = switch (walletInfo.type) {
118 + WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}',
119 + WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}',
120 + _ => '${walletKey}_${Erc20Token.ethereumBoxName}',
121 + };
122 +
123 + if (CakeHive.isBoxOpen(boxName)) {
124 + return CakeHive.box<Erc20Token>(boxName);
125 + }
126 + return CakeHive.openBox<Erc20Token>(boxName);
127 + }
128 +
129 + static Future<Box<SPLToken>> _openSolTokensBoxFor(WalletInfo wallet) async {
130 + final boxName = '${wallet.name.replaceAll(' ', '_')}_${SPLToken.boxName}';
131 + if (CakeHive.isBoxOpen(boxName)) {
132 + return CakeHive.box<SPLToken>(boxName);
133 + }
134 + return CakeHive.openBox<SPLToken>(boxName);
135 + }
136 +
137 + static Future<Box<TronToken>> _openTronTokensBoxFor(
138 + WalletInfo walletInfo,
139 + ) async {
140 + final boxName = '${walletInfo.name.replaceAll(' ', '_')}_${TronToken.boxName}';
141 + if (CakeHive.isBoxOpen(boxName)) {
142 + return CakeHive.box<TronToken>(boxName);
143 + }
144 + return CakeHive.openBox<TronToken>(boxName);
145 + }
146 +
147 + static Erc20Token? findErc20Token(CryptoCurrency currency, WalletBase wallet) {
148 + if (currency is Erc20Token) return currency;
149 +
150 + // More of a fallback for us
151 + for (final balanceCurrency in wallet.balance.keys) {
152 + if (balanceCurrency is Erc20Token && _matchesToken(balanceCurrency, currency)) {
153 + return balanceCurrency;
154 + }
155 + }
156 +
157 + return null;
158 + }
159 +
160 + static bool isNativeToken(CryptoCurrency currency) {
161 + final title = currency.title.toLowerCase();
162 + final tag = currency.tag?.toLowerCase();
163 +
164 + return title == 'eth' ||
165 + title == 'ethereum' ||
166 + title == 'matic' ||
167 + title == 'polygon' ||
168 + title == 'bnb' ||
169 + title == 'bsc' ||
170 + title == 'avax' ||
171 + title == 'avalanche' ||
172 + tag == 'polygon' ||
173 + tag == 'bsc' ||
174 + tag == 'avalanche';
175 + }
176 +
177 + static int getChainId(CryptoCurrency currency) {
178 + final title = currency.title.toLowerCase();
179 + final tag = currency.tag?.toLowerCase();
180 +
181 + // Polygon
182 + if (title == 'polygon' || title == 'matic' || tag == 'polygon') {
183 + return 137;
184 + }
185 +
186 + // BSC (Binance Smart Chain)
187 + if (title == 'bsc' || title == 'bnb' || tag == 'bsc') {
188 + return 56;
189 + }
190 +
191 + // Avalanche C-Chain
192 + if (title == 'avalanche' || title == 'avax' || tag == 'avalanche') {
193 + return 43114;
194 + }
195 +
196 + // Arbitrum One
197 + if (title == 'arbitrum' || title == 'arb' || tag == 'arbitrum') {
198 + return 42161;
199 + }
200 +
201 + // Optimism
202 + if (title == 'optimism' || title == 'op' || tag == 'optimism') {
203 + return 10;
204 + }
205 +
206 + // Base
207 + if (title == 'base' || tag == 'base') {
208 + return 8453;
209 + }
210 +
211 + // Fantom Opera
212 + if (title == 'fantom' || title == 'ftm' || tag == 'fantom') {
213 + return 250;
214 + }
215 +
216 + // Default to Ethereum mainnet
217 + return 1;
218 + }
219 +
220 + /// Checks if two currencies match (by title and tag)
221 + static bool _matchesToken(Erc20Token token, CryptoCurrency currency) {
222 + return token.title.toLowerCase() == currency.title.toLowerCase() &&
223 + (token.tag?.toLowerCase() == currency.tag?.toLowerCase() ||
224 + (token.tag == null && currency.tag == null));
225 + }
226 +}
lib/view_model/dev/exchange_provider_logs_view_model.dart new
+84
@@ -0,0 +1,84 @@
1 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
2 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
3 +import 'package:mobx/mobx.dart';
4 +
5 +part 'exchange_provider_logs_view_model.g.dart';
6 +
7 +enum LogFilter { all, success, error }
8 +
9 +class ExchangeProviderLogsViewModel = ExchangeProviderLogsViewModelBase with _$ExchangeProviderLogsViewModel;
10 +
11 +abstract class ExchangeProviderLogsViewModelBase with Store {
12 + @observable
13 + bool isLoading = false;
14 +
15 + @observable
16 + String? error;
17 +
18 + @observable
19 + ObservableList<ExchangeProviderLogEntry> logs = ObservableList<ExchangeProviderLogEntry>();
20 +
21 + @observable
22 + LogFilter currentFilter = LogFilter.all;
23 +
24 + @computed
25 + ObservableList<ExchangeProviderLogEntry> get filteredLogs {
26 + switch (currentFilter) {
27 + case LogFilter.all:
28 + return logs;
29 + case LogFilter.success:
30 + return ObservableList.of(logs.where((log) => log.isSuccess).toList());
31 + case LogFilter.error:
32 + return ObservableList.of(logs.where((log) => !log.isSuccess).toList());
33 + }
34 + }
35 +
36 + @computed
37 + int get totalLogs => logs.length;
38 +
39 + @computed
40 + int get successLogs => logs.where((log) => log.isSuccess).length;
41 +
42 + @computed
43 + int get errorLogs => logs.where((log) => !log.isSuccess).length;
44 +
45 + @computed
46 + Map<ExchangeProviderDescription, int> get logsByProvider {
47 + final Map<ExchangeProviderDescription, int> counts = {};
48 + for (final log in logs) {
49 + counts[log.provider] = (counts[log.provider] ?? 0) + 1;
50 + }
51 + return counts;
52 + }
53 +
54 + String getLogsAsText() => ExchangeProviderLogger.getLogsAsText();
55 +
56 + String getLogsAsJson() => ExchangeProviderLogger.getLogsAsJson();
57 +
58 + @action
59 + void loadLogs() {
60 + isLoading = true;
61 + error = null;
62 +
63 + try {
64 + logs.clear();
65 + logs.addAll(ExchangeProviderLogger.logs);
66 + } catch (e) {
67 + error = e.toString();
68 + } finally {
69 + isLoading = false;
70 + }
71 + }
72 +
73 + @action
74 + void clearLogs() {
75 + ExchangeProviderLogger.clearLogs();
76 + logs.clear();
77 + }
78 +
79 + @action
80 + void refreshLogs() => loadLogs();
81 +
82 + @action
83 + void setFilter(LogFilter filter) => currentFilter = filter;
84 +}
lib/view_model/exchange/exchange_trade_view_model.dart
+76
@@ -1,5 +1,6 @@
1 import 'dart:async';
2
3 +import 'package:cake_wallet/core/payment_uris.dart';
4 import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
5 import 'package:cake_wallet/entities/fiat_currency.dart';
6 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
@@ -20,12 +21,14 @@ import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'
21 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
22 import 'package:cake_wallet/store/dashboard/trades_store.dart';
23 import 'package:cake_wallet/utils/qr_util.dart';
24 +import 'package:cake_wallet/utils/token_utilities.dart';
25 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
26 import 'package:cake_wallet/view_model/send/output.dart';
27 import 'package:cake_wallet/view_model/send/send_view_model.dart';
28 import 'package:cw_core/crypto_currency.dart';
29 import 'package:cw_core/utils/print_verbose.dart';
30 import 'package:cw_core/wallet_base.dart';
31 +import 'package:cw_core/wallet_type.dart';
32 import 'package:hive/hive.dart';
33 import 'package:mobx/mobx.dart';
34
@@ -317,6 +320,79 @@ abstract class ExchangeTradeViewModelBase with Store {
320 _isTronToken();
321 }
322
323 + PaymentURI? get paymentUri {
324 + final inputAddress = trade.inputAddress;
325 + final amount = trade.amount;
326 + final fromCurrency = trade.from ?? trade.userCurrencyFrom;
327 +
328 + if (inputAddress == null || inputAddress.isEmpty || fromCurrency == null) {
329 + return null;
330 + }
331 +
332 + switch (wallet.type) {
333 + case WalletType.bitcoin:
334 + return BitcoinURI(amount: amount, address: inputAddress);
335 + case WalletType.litecoin:
336 + return LitecoinURI(amount: amount, address: inputAddress);
337 + case WalletType.bitcoinCash:
338 + return BitcoinCashURI(amount: amount, address: inputAddress);
339 + case WalletType.dogecoin:
340 + return DogeURI(amount: amount, address: inputAddress);
341 + case WalletType.ethereum:
342 + return _createERC681URI(fromCurrency, inputAddress, amount);
343 + // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling)
344 + case WalletType.polygon:
345 + return PolygonURI(amount: amount, address: inputAddress);
346 + case WalletType.solana:
347 + return SolanaURI(amount: amount, address: inputAddress);
348 + case WalletType.tron:
349 + return TronURI(amount: amount, address: inputAddress);
350 + case WalletType.monero:
351 + return MoneroURI(amount: amount, address: inputAddress);
352 + case WalletType.wownero:
353 + return WowneroURI(amount: amount, address: inputAddress);
354 + case WalletType.zano:
355 + return ZanoURI(amount: amount, address: inputAddress);
356 + case WalletType.decred:
357 + return DecredURI(amount: amount, address: inputAddress);
358 + case WalletType.haven:
359 + return HavenURI(amount: amount, address: inputAddress);
360 + case WalletType.nano:
361 + return NanoURI(amount: amount, address: inputAddress);
362 + default:
363 + return null;
364 + }
365 + }
366 +
367 + @action
368 + PaymentURI? _createERC681URI(CryptoCurrency currency, String address, String amount) {
369 + final chainId = TokenUtilities.getChainId(currency);
370 + final isNativeToken = TokenUtilities.isNativeToken(currency);
371 +
372 + if (isNativeToken) {
373 + return ERC681URI(
374 + chainId: chainId,
375 + address: address,
376 + amount: amount,
377 + contractAddress: null,
378 + );
379 + } else {
380 + if (wallet.type == WalletType.polygon || wallet.type == WalletType.ethereum) {
381 + final erc20Token = TokenUtilities.findErc20Token(currency, wallet);
382 +
383 + if (erc20Token != null) {
384 + return ERC681URI(
385 + chainId: chainId,
386 + address: address,
387 + amount: amount,
388 + contractAddress: erc20Token.contractAddress,
389 + );
390 + }
391 + }
392 + return null;
393 + }
394 + }
395 +
396 @computed
397 String get qrImage => getQrImage(wallet.type);
398 }
lib/view_model/exchange/exchange_view_model.dart
+42 -104
@@ -39,10 +39,10 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart';
39 import 'package:cake_wallet/store/settings_store.dart';
40 import 'package:cake_wallet/store/templates/exchange_template_store.dart';
41 import 'package:cake_wallet/utils/feature_flag.dart';
42 +import 'package:cake_wallet/utils/token_utilities.dart';
43 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
44 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
45 import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
45 -import 'package:cw_core/cake_hive.dart';
46 import 'package:cw_core/crypto_currency.dart';
47 import 'package:cw_core/erc20_token.dart';
48 import 'package:cw_core/spl_token.dart';
@@ -142,7 +142,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
142 }
143 });
144
145 - bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => calculateBestRate());
145 + bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) {
146 + if (tradeState is! TradeIsCreating) {
147 + calculateBestRate();
148 + }
149 + });
150
151 isDepositAddressEnabled = !(depositCurrency == wallet.currency);
152 depositAmount = '';
@@ -536,19 +540,27 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
540 ),
541 ),
542 );
539 - _sortedAvailableProviders.clear();
543 +
544 + // We'll use a new SplayTreeMap to avoid concurrent modification issues
545 + final newSortedProviders =
546 + SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a));
547
548 for (int i = 0; i < result.length; i++) {
549 if (result[i] != 0) {
550 /// add this provider as its valid for this trade
551 try {
545 - _sortedAvailableProviders[result[i]] = _providers[i];
552 + newSortedProviders[result[i]] = _providers[i];
553 } catch (e) {
554 // will throw "Concurrent modification during iteration" error if modified at the same
555 // time [createTrade] is called, as this is not a normal map, but a sorted map
556 }
557 }
558 }
559 +
560 + // Replace the old map with the new one
561 + _sortedAvailableProviders.clear();
562 + _sortedAvailableProviders.addAll(newSortedProviders);
563 +
564 if (_sortedAvailableProviders.isNotEmpty) bestRate = _sortedAvailableProviders.keys.first;
565 }
566
@@ -637,10 +649,26 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
649 }
650 }
651
652 + // Ensure we have providers available before attempting to create trade
653 + if (_sortedAvailableProviders.isEmpty) {
654 + await calculateBestRate();
655 +
656 + if (_sortedAvailableProviders.isEmpty) {
657 + tradeState = TradeIsCreatedFailure(
658 + title: S.current.trade_not_created,
659 + error: S.current.none_of_selected_providers_can_exchange);
660 + return;
661 + }
662 + }
663 +
664 try {
641 - for (var i = 0; i < _sortedAvailableProviders.values.length; i++) {
642 - final provider = _sortedAvailableProviders.values.toList()[i];
643 - final providerRate = _sortedAvailableProviders.keys.toList()[i];
665 + // snapshot of providers to avoid concurrent modification issues
666 + final providersSnapshot = _sortedAvailableProviders.values.toList();
667 + final ratesSnapshot = _sortedAvailableProviders.keys.toList();
668 +
669 + for (var i = 0; i < providersSnapshot.length; i++) {
670 + final provider = providersSnapshot[i];
671 + final providerRate = ratesSnapshot[i];
672
673 if (!(await provider.checkIsAvailable())) continue;
674
@@ -675,8 +703,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
703 } else {
704 try {
705 if (provider is SwapTradeExchangeProvider) {
678 - final destinationAmount = (fiatConversionStore.prices[request.toCurrency] ?? 0.00) * (double.tryParse(request.toAmount) ?? 0.00);
679 - final sendingAmount = (fiatConversionStore.prices[request.fromCurrency] ?? 0.00) * (double.tryParse(request.fromAmount) ?? 0.00);
706 + final destinationAmount = (fiatConversionStore.prices[request.toCurrency] ?? 0.00) *
707 + (double.tryParse(request.toAmount) ?? 0.00);
708 + final sendingAmount = (fiatConversionStore.prices[request.fromCurrency] ?? 0.00) *
709 + (double.tryParse(request.fromAmount) ?? 0.00);
710
711 if (destinationAmount > 2000 || sendingAmount > 2000) {
712 continue;
@@ -1066,47 +1096,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1096
1097 // Adding user's Erc20 tokens to the list of currencies
1098
1069 - Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
1070 - final walletKey = walletInfo.name.replaceAll(" ", "_");
1071 -
1072 - final boxName = switch (walletInfo.type) {
1073 - WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}',
1074 - WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}',
1075 - _ => '${walletKey}_${Erc20Token.ethereumBoxName}',
1076 - };
1077 -
1078 - if (CakeHive.isBoxOpen(boxName)) {
1079 - return CakeHive.box<Erc20Token>(boxName);
1080 - }
1081 - return CakeHive.openBox<Erc20Token>(boxName);
1082 - }
1083 -
1084 - Future<List<Erc20Token>> _loadAllUniqueEvmTokens() async {
1085 - final evmWallets = walletInfoSource.values.where(
1086 - (w) => w.type == WalletType.ethereum || w.type == WalletType.polygon,
1087 - );
1088 -
1089 - final seen = <String>{};
1090 - final unique = <Erc20Token>[];
1091 -
1092 - for (final wallet in evmWallets) {
1093 - final chain = wallet.type == WalletType.ethereum ? 'ETH' : 'POL';
1094 - final box = await _openEvmTokensBoxFor(wallet);
1095 -
1096 - for (final t in box.values.where((t) => t.enabled)) {
1097 - final key = '$chain|${t.contractAddress.toLowerCase()}';
1098 - if (seen.add(key)) {
1099 - unique.add(t);
1100 - }
1101 - }
1102 - }
1103 -
1104 - return unique;
1105 - }
1106 -
1099 @action
1100 Future<void> _injectUserEthTokensIntoCurrencyLists() async {
1109 - final userTokens = await _loadAllUniqueEvmTokens();
1101 + final userTokens = await TokenUtilities.loadAllUniqueEvmTokens(walletInfoSource);
1102
1103 final toAddReceive = <CryptoCurrency>[];
1104 final toAddDeposit = <CryptoCurrency>[];
@@ -1132,35 +1124,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1124
1125 // Adding user's Solana tokens to the list of currencies
1126
1135 - Future<Box<SPLToken>> _openSolTokensBoxFor(WalletInfo wallet) async {
1136 - final boxName = '${wallet.name.replaceAll(" ", "_")}_${SPLToken.boxName}';
1137 - if (CakeHive.isBoxOpen(boxName)) {
1138 - return CakeHive.box<SPLToken>(boxName);
1139 - }
1140 - return CakeHive.openBox<SPLToken>(boxName);
1141 - }
1142 -
1143 - Future<List<SPLToken>> _loadAllUniqueSolTokens() async {
1144 - final solWallets = walletInfoSource.values.where((wallet) => wallet.type == WalletType.solana);
1145 - final tokens = <SPLToken>[];
1146 -
1147 - for (final wallet in solWallets) {
1148 - final box = await _openSolTokensBoxFor(wallet);
1149 - tokens.addAll(box.values.where((t) => t.enabled));
1150 - }
1151 -
1152 - final seen = <String>{};
1153 - final unique = <SPLToken>[];
1154 - for (final token in tokens) {
1155 - final key = token.mintAddress.toLowerCase();
1156 - if (!seen.contains(key)) {
1157 - seen.add(key);
1158 - unique.add(token);
1159 - }
1160 - }
1161 - return unique;
1162 - }
1163 -
1127 bool _listContainsSplToken(List<CryptoCurrency> list, SPLToken token) {
1128 return list.any((item) {
1129 if (item is SPLToken) {
@@ -1173,7 +1136,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1136
1137 @action
1138 Future<void> _injectUserSplTokensIntoCurrencyLists() async {
1176 - final userTokens = await _loadAllUniqueSolTokens();
1139 + final userTokens = await TokenUtilities.loadAllUniqueSolTokens(walletInfoSource);
1140
1141 final toAddReceive = <CryptoCurrency>[];
1142 final toAddDeposit = <CryptoCurrency>[];
@@ -1189,31 +1152,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1152
1153 // Adding user's Tron tokens to the list of currencies
1154
1192 - Future<Box<TronToken>> _openTronTokensBoxFor(WalletInfo walletInfo) async {
1193 - final boxName = '${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}';
1194 - if (CakeHive.isBoxOpen(boxName)) {
1195 - return CakeHive.box<TronToken>(boxName);
1196 - }
1197 - return CakeHive.openBox<TronToken>(boxName);
1198 - }
1199 -
1200 - Future<List<TronToken>> _loadAllUniqueTronTokens() async {
1201 - final tronWallets = walletInfoSource.values.where((w) => w.type == WalletType.tron);
1202 -
1203 - final seen = <String>{};
1204 - final unique = <TronToken>[];
1205 -
1206 - for (final wallet in tronWallets) {
1207 - final box = await _openTronTokensBoxFor(wallet);
1208 - for (final t in box.values.where((t) => t.enabled)) {
1209 - final key = t.contractAddress.toLowerCase();
1210 - if (seen.add(key)) unique.add(t);
1211 - }
1212 - }
1213 -
1214 - return unique;
1215 - }
1216 -
1155 bool _listContainsTronToken(List<CryptoCurrency> list, TronToken token) {
1156 return list.any((item) {
1157 if (item is TronToken) {
@@ -1226,7 +1164,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1164
1165 @action
1166 Future<void> _injectUserTronTokensIntoCurrencyLists() async {
1229 - final userTokens = await _loadAllUniqueTronTokens();
1167 + final userTokens = await TokenUtilities.loadAllUniqueTronTokens(walletInfoSource);
1168
1169 final toAddReceive = <CryptoCurrency>[];
1170 final toAddDeposit = <CryptoCurrency>[];
lib/view_model/send/send_view_model.dart
+28 -8
@@ -37,9 +37,7 @@ import 'package:cake_wallet/utils/payment_request.dart';
37 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
38 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
39 import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
40 -import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
40 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
42 -import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
41 import 'package:cake_wallet/view_model/send/output.dart';
42 import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
43 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
@@ -53,10 +51,12 @@ import 'package:cw_core/sync_status.dart';
51 import 'package:cw_core/transaction_info.dart';
52 import 'package:cw_core/unspent_coin_type.dart';
53 import 'package:cw_core/utils/print_verbose.dart';
54 +import 'package:cw_core/wallet_info.dart';
55 import 'package:cw_core/wallet_type.dart';
56 import 'package:flutter/material.dart';
57 import 'package:hive/hive.dart';
58 import 'package:mobx/mobx.dart';
59 +import 'package:cake_wallet/utils/token_utilities.dart';
60 import 'package:shared_preferences/shared_preferences.dart';
61
62 part 'send_view_model.g.dart';
@@ -72,14 +72,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
72 wallet.type == WalletType.solana ||
73 wallet.type == WalletType.tron ||
74 wallet.type == WalletType.zano;
75 -
75 +
76 for (final output in outputs) {
77 output.updateWallet(wallet);
78 }
79 -
79 +
80 // Update unspent coins list view model with the new wallet reference
81 unspentCoinsListViewModel.updateWallet(wallet);
82 -
82 +
83 // Update sending balance to reflect the new wallet's balance
84 updateSendingBalance();
85 }
@@ -95,7 +95,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
95 this.transactionDescriptionBox,
96 this.ledgerViewModel,
97 this.unspentCoinsListViewModel,
98 - this.feesViewModel, {
98 + this.feesViewModel,
99 + this.walletInfoSource, {
100 this.coinTypeToSpendFrom = UnspentCoinType.nonMweb,
101 }) : state = InitialExecutionState(),
102 currencies = appStore.wallet!.balance.keys.toList(),
@@ -121,6 +122,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
122
123 ObservableList<Output> outputs;
124
125 + final Box<WalletInfo> walletInfoSource;
126 +
127 @observable
128 UnspentCoinType coinTypeToSpendFrom;
129
@@ -337,8 +340,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
340 ].contains(wallet.type);
341
342 @computed
340 - bool get isElectrumWallet =>
341 - [WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, WalletType.dogecoin].contains(wallet.type);
343 + bool get isElectrumWallet => [
344 + WalletType.bitcoin,
345 + WalletType.litecoin,
346 + WalletType.bitcoinCash,
347 + WalletType.dogecoin
348 + ].contains(wallet.type);
349
350 @observable
351 CryptoCurrency selectedCryptoCurrency;
@@ -924,4 +931,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
931
932 @observable
933 String? payjoinUri;
934 +
935 + @action
936 + Future<void> fetchTokenForContractAddress(String contractAddress) async {
937 + final token = await TokenUtilities.findTokenByAddress(
938 + walletType: wallet.type,
939 + walletInfoSource: walletInfoSource,
940 + address: contractAddress,
941 + );
942 +
943 + if (token != null) {
944 + selectedCryptoCurrency = token as CryptoCurrency;
945 + }
946 + }
947 }
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+1 -222
@@ -3,6 +3,7 @@ import 'dart:core';
3
4 import 'package:cake_wallet/bitcoin/bitcoin.dart';
5 import 'package:cake_wallet/core/fiat_conversion_service.dart';
6 +import 'package:cake_wallet/core/payment_uris.dart';
7 import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
8 import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
9 import 'package:cake_wallet/entities/fiat_api_mode.dart';
@@ -37,228 +38,6 @@ part 'wallet_address_list_view_model.g.dart';
38
39 class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel;
40
40 -abstract class PaymentURI {
41 - PaymentURI({required this.amount, required this.address});
42 -
43 - final String amount;
44 - final String address;
45 -}
46 -
47 -class MoneroURI extends PaymentURI {
48 - MoneroURI({required super.amount, required super.address});
49 -
50 - @override
51 - String toString() {
52 - var base = 'monero:$address';
53 -
54 - if (amount.isNotEmpty) {
55 - base += '?tx_amount=${amount.replaceAll(',', '.')}';
56 - }
57 -
58 - return base;
59 - }
60 -}
61 -
62 -class HavenURI extends PaymentURI {
63 - HavenURI({required super.amount, required super.address});
64 -
65 - @override
66 - String toString() {
67 - var base = 'haven:$address';
68 -
69 - if (amount.isNotEmpty) {
70 - base += '?tx_amount=${amount.replaceAll(',', '.')}';
71 - }
72 -
73 - return base;
74 - }
75 -}
76 -
77 -class BitcoinURI extends PaymentURI {
78 - BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
79 -
80 - final String pjUri;
81 -
82 - @override
83 - String toString() {
84 - final qp = <String, String>{};
85 -
86 - if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
87 - if (pjUri.isNotEmpty && !address.startsWith("sp")) {
88 - qp['pjos'] = '0';
89 - qp['pj'] = pjUri;
90 - }
91 -
92 - return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
93 - }
94 -}
95 -
96 -class LitecoinURI extends PaymentURI {
97 - LitecoinURI({required super.amount, required super.address});
98 -
99 - @override
100 - String toString() {
101 - var base = 'litecoin:$address';
102 -
103 - if (amount.isNotEmpty) {
104 - base += '?amount=${amount.replaceAll(',', '.')}';
105 - }
106 -
107 - return base;
108 - }
109 -}
110 -
111 -class EthereumURI extends PaymentURI {
112 - EthereumURI({required super.amount, required super.address});
113 -
114 - @override
115 - String toString() {
116 - var base = 'ethereum:$address';
117 -
118 - if (amount.isNotEmpty) {
119 - base += '?amount=${amount.replaceAll(',', '.')}';
120 - }
121 -
122 - return base;
123 - }
124 -}
125 -
126 -class BitcoinCashURI extends PaymentURI {
127 - BitcoinCashURI({required super.amount, required super.address});
128 -
129 - @override
130 - String toString() {
131 - var base = address;
132 -
133 - if (amount.isNotEmpty) {
134 - base += '?amount=${amount.replaceAll(',', '.')}';
135 - }
136 -
137 - return base;
138 - }
139 -}
140 -
141 -class NanoURI extends PaymentURI {
142 - NanoURI({required super.amount, required super.address});
143 -
144 - @override
145 - String toString() {
146 - var base = 'nano:$address';
147 - if (amount.isNotEmpty) {
148 - base += '?amount=${amount.replaceAll(',', '.')}';
149 - }
150 -
151 - return base;
152 - }
153 -}
154 -
155 -class PolygonURI extends PaymentURI {
156 - PolygonURI({required super.amount, required super.address});
157 -
158 - @override
159 - String toString() {
160 - var base = 'polygon:$address';
161 -
162 - if (amount.isNotEmpty) {
163 - base += '?amount=${amount.replaceAll(',', '.')}';
164 - }
165 -
166 - return base;
167 - }
168 -}
169 -
170 -class SolanaURI extends PaymentURI {
171 - SolanaURI({required super.amount, required super.address});
172 -
173 - @override
174 - String toString() {
175 - var base = 'solana:$address';
176 -
177 - if (amount.isNotEmpty) {
178 - base += '?amount=${amount.replaceAll(',', '.')}';
179 - }
180 -
181 - return base;
182 - }
183 -}
184 -
185 -class TronURI extends PaymentURI {
186 - TronURI({required super.amount, required super.address});
187 -
188 - @override
189 - String toString() {
190 - var base = 'tron:$address';
191 -
192 - if (amount.isNotEmpty) {
193 - base += '?amount=${amount.replaceAll(',', '.')}';
194 - }
195 -
196 - return base;
197 - }
198 -}
199 -
200 -class WowneroURI extends PaymentURI {
201 - WowneroURI({required super.amount, required super.address});
202 -
203 - @override
204 - String toString() {
205 - var base = 'wownero:$address';
206 -
207 - if (amount.isNotEmpty) {
208 - base += '?tx_amount=${amount.replaceAll(',', '.')}';
209 - }
210 -
211 - return base;
212 - }
213 -}
214 -
215 -class ZanoURI extends PaymentURI {
216 - ZanoURI({required String amount, required String address})
217 - : super(amount: amount, address: address);
218 -
219 - @override
220 - String toString() {
221 - var base = 'zano:' + address;
222 -
223 - if (amount.isNotEmpty) {
224 - base += '?amount=${amount.replaceAll(',', '.')}';
225 - }
226 -
227 - return base;
228 - }
229 -}
230 -
231 -class DecredURI extends PaymentURI {
232 - DecredURI({required String amount, required String address})
233 - : super(amount: amount, address: address);
234 -
235 - @override
236 - String toString() {
237 - var base = 'decred:' + address;
238 -
239 - if (amount.isNotEmpty) {
240 - base += '?amount=${amount.replaceAll(',', '.')}';
241 - }
242 -
243 - return base;
244 - }
245 -}
246 -
247 -class DogeURI extends PaymentURI {
248 - DogeURI({required String amount, required String address})
249 - : super(amount: amount, address: address);
250 -
251 - @override
252 - String toString() {
253 - var base = 'doge:' + address;
254 -
255 - if (amount.isNotEmpty) {
256 - base += '?amount=${amount.replaceAll(',', '.')}';
257 - }
258 -
259 - return base;
260 - }
261 -}
41
42 abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store {
43 WalletAddressListViewModelBase({