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>
This commit is contained in:
David Adegoke
2025-09-24 20:24:52 +03:00
committed by GitHub
co-authored by Omar Hatem
parent a84c563f89
commit c51c2dc105
29 changed files with 2919 additions and 473 deletions
+410
View File
@@ -0,0 +1,410 @@
import 'package:cw_core/format_fixed.dart';
abstract class PaymentURI {
PaymentURI({required this.amount, required this.address});
final String amount;
final String address;
}
class MoneroURI extends PaymentURI {
MoneroURI({required super.amount, required super.address});
@override
String toString() {
var base = 'monero:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class HavenURI extends PaymentURI {
HavenURI({required super.amount, required super.address});
@override
String toString() {
var base = 'haven:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BitcoinURI extends PaymentURI {
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
final String pjUri;
@override
String toString() {
final qp = <String, String>{};
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
qp['pjos'] = '0';
qp['pj'] = pjUri;
}
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
}
}
class LitecoinURI extends PaymentURI {
LitecoinURI({required super.amount, required super.address});
@override
String toString() {
var base = 'litecoin:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class EthereumURI extends PaymentURI {
EthereumURI({required super.amount, required super.address});
@override
String toString() {
var base = 'ethereum:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BitcoinCashURI extends PaymentURI {
BitcoinCashURI({required super.amount, required super.address});
@override
String toString() {
var base = address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class NanoURI extends PaymentURI {
NanoURI({required super.amount, required super.address});
@override
String toString() {
var base = 'nano:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class PolygonURI extends PaymentURI {
PolygonURI({required super.amount, required super.address});
@override
String toString() {
var base = 'polygon:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class SolanaURI extends PaymentURI {
SolanaURI({required super.amount, required super.address});
@override
String toString() {
var base = 'solana:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class TronURI extends PaymentURI {
TronURI({required super.amount, required super.address});
@override
String toString() {
var base = 'tron:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class WowneroURI extends PaymentURI {
WowneroURI({required super.amount, required super.address});
@override
String toString() {
var base = 'wownero:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class ZanoURI extends PaymentURI {
ZanoURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'zano:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class DecredURI extends PaymentURI {
DecredURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'decred:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class DogeURI extends PaymentURI {
DogeURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'doge:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class ERC681URI extends PaymentURI {
final int chainId;
final String? contractAddress;
ERC681URI({
required this.chainId,
required super.address,
required super.amount,
required this.contractAddress,
});
@override
String toString() {
var uri = 'ethereum:';
final targetAddress = contractAddress ?? address;
uri += targetAddress;
if (chainId != 1) {
uri += '@$chainId';
}
if (contractAddress != null) {
uri += '/transfer';
}
final params = <String, String>{};
if (contractAddress != null) {
params['address'] = address;
if (amount.isNotEmpty) {
params['uint256'] = _formatAmountForERC20(amount);
}
} else {
if (amount.isNotEmpty) {
params['value'] = _formatAmountForNative(amount);
}
}
if (params.isNotEmpty) {
uri += '?';
uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
}
return uri;
}
/// Formats amount for ERC-20 token transfers (in atomic units)
String _formatAmountForERC20(String amount) {
try {
// Convert decimal amount to BigInt (assuming 18 decimals)
final amountDouble = double.parse(amount.replaceAll(',', '.'));
final amountBigInt = BigInt.from(amountDouble * 1e18);
return amountBigInt.toString();
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(',', '.');
}
}
/// Formats amount for native ETH payments (in wei using scientific notation)
String _formatAmountForNative(String amount) {
try {
// Convert decimal amount to double for scientific notation
final amountDouble = double.parse(amount.replaceAll(',', '.'));
// Use scientific notation as recommended by ERC-681
return '${amountDouble}e18';
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(',', '.');
}
}
factory ERC681URI.fromUri(Uri uri) {
final (isContract, targetAddress) = _getTargetAddress(uri.path);
final chainId = _getChainID(uri.path);
final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
var formatedAmount = "";
if (amountParam != null) {
final normalized = _normalizeToIntegerWei(amountParam);
formatedAmount = formatFixed(BigInt.parse(normalized), 18);
} else {
formatedAmount = uri.queryParameters["amount"] ?? "";
}
return ERC681URI(
chainId: chainId,
address: address,
amount: formatedAmount,
contractAddress: isContract ? targetAddress : null,
);
}
static int _getChainID(String path) {
return int.parse(RegExp(
r'@\d*',
).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
"1");
}
static (bool, String) _getTargetAddress(String path) {
final targetAddress =
RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!;
return (path.contains("/"), targetAddress);
}
/// Normalize an input amount into an integer wei string.
///
/// Accepts the following forms:
/// - Integer string: "123000000000000000" → unchanged
/// - Scientific notation: "0.123e18", "1e6" → expanded to integer
/// - Decimal ETH: "0.123456" → shifted by 18 decimals
static String _normalizeToIntegerWei(String input) {
final raw = input.replaceAll(',', '.').trim();
// First we check if it's already a plain integer (basically just a number with no dot, no exponent)
try {
final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) &&
!raw.contains('.') &&
!raw.toLowerCase().contains('e');
if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), '');
// Then we check if it's a scientific notation
final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$');
if (sci.hasMatch(raw)) {
final mantissaStr = raw.toLowerCase().split('e')[0];
final exp = int.parse(raw.toLowerCase().split('e')[1]);
return _expandDecimal(mantissaStr, exp);
}
// Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount
if (raw.contains('.')) {
return _expandDecimal(raw, 18);
}
return raw;
} catch (e) {
return raw;
}
// If none of these checks work, we return the raw input
}
/// Expands a decimal string by shifting the decimal point `expShift` places
/// to the right and returns an integer string (digits only, optional leading minus).
/// Examples:
/// _expandDecimal('0.123456', 18) -> '123456000000000000'
/// _expandDecimal('1.2', 3) -> '1200'
static String _expandDecimal(String decimalStr, int expShift) {
var s = decimalStr.trim();
var sign = '';
if (s.startsWith('-') || s.startsWith('+')) {
sign = s[0] == '-' ? '-' : '';
s = s.substring(1);
}
// First we split the integer and fractional parts
final parts = s.split('.');
final intPart = parts[0].isEmpty ? '0' : parts[0];
final fracPart = parts.length > 1 ? parts[1] : '';
final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), '');
final fracLen = fracPart.length;
// Then we calculate the effective shift = desired shift minus existing fractional digits
final shift = expShift - fracLen;
if (shift >= 0) {
final head = digits.isEmpty ? '0' : digits;
final zeros = List.filled(shift, '0').join();
final res = head + zeros;
return sign + (res.isEmpty ? '0' : res);
} else {
// Need to insert a decimal point within digits; return integer by truncating
final cut = digits.length + shift;
if (cut <= 0) {
return '0';
}
final res = digits.substring(0, cut);
return sign + (res.isEmpty ? '0' : res);
}
}
}
+25 -23
View File
@@ -1,4 +1,5 @@
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
@@ -104,15 +105,30 @@ class UniversalAddressDetector {
currency: CryptoCurrency.btcln,
),
// Bitcoin Bech32
// Bitcoin (P2PKH, P2SH, Bech32, Silent Payments)
_DetectionPattern(
pattern: RegExp(r'^bc1[a-km-zA-HJ-NP-Z1-9]{25,39}$'),
pattern: RegExp('^(?:'
'1[a-km-zA-HJ-NP-Z1-9]{25,34}'
'|3[a-km-zA-HJ-NP-Z1-9]{25,34}'
'|(?:bc|tb)1q[ac-hj-np-z02-9]{25,39}'
'|(?:bc|tb)1p(?:[ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89})'
'|(?:bc|tb)1q[ac-hj-np-z02-9]{40,80}'
'|${AddressValidator.silentPaymentAddressPatternMainnet}'
'|${AddressValidator.silentPaymentAddressPatternTestnet}'
')\$'),
currency: CryptoCurrency.btc,
),
// Litecoin Bech32
// Litecoin (Legacy, Bech32, MWEB)
_DetectionPattern(
pattern: RegExp(r'^ltc1[a-z0-9]{25,50}$'),
pattern: RegExp('^(?:'
'L[a-km-zA-HJ-NP-Z1-9]{25,34}'
'|[3M][a-km-zA-HJ-NP-Z1-9]{25,34}'
'|ltc1q[ac-hj-np-z02-9]{25,39}'
'|ltc1p(?:[ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89})'
'|ltc1q[ac-hj-np-z02-9]{40,80}'
'|${AddressValidator.mWebAddressPattern}'
')\$'),
currency: CryptoCurrency.ltc,
),
@@ -182,31 +198,17 @@ class UniversalAddressDetector {
currency: CryptoCurrency.dcr,
),
// Bitcoin P2PKH/P2SH (legacy formats)
_DetectionPattern(
pattern: RegExp(r'^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$'),
currency: CryptoCurrency.btc,
),
// Litecoin P2PKH/P2SH
_DetectionPattern(
pattern: RegExp(r'^(L|M|3)[a-km-zA-HJ-NP-Z1-9]{25,34}$'),
currency: CryptoCurrency.ltc,
),
// Dogecoin P2PKH
_DetectionPattern(
pattern: RegExp(r'^D[a-km-zA-HJ-NP-Z1-9]{25,34}$'),
currency: CryptoCurrency.doge,
),
// TODO: commented out until a better approach is implemented.
// as this will consider most addresses to be a valid Solana address
// Solana (Base58 format, 32-44 chars)
// _DetectionPattern(
// pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{32,44}$'),
// currency: CryptoCurrency.sol,
// ),
// Solana (Base58 format)
_DetectionPattern(
pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{43,44}$'),
currency: CryptoCurrency.sol,
),
];
// Test each pattern in order of specificity
+6
View File
@@ -36,6 +36,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart';
@@ -287,6 +288,7 @@ import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/core/reset_service.dart';
import 'package:cake_wallet/view_model/dev/socket_health_logs_view_model.dart';
import 'package:cake_wallet/src/screens/dev/socket_health_logs_page.dart';
import 'package:cake_wallet/view_model/dev/exchange_provider_logs_view_model.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
@@ -818,6 +820,7 @@ Future<void> setup({
coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.nonMweb,
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom),
getIt.get<FeesViewModel>(),
_walletInfoSource,
),
);
@@ -1566,6 +1569,9 @@ Future<void> setup({
getIt.registerFactory(() => DevNetworkRequests());
getIt.registerFactory(() => ExchangeProviderLogsViewModel());
getIt.registerFactory(() => DevExchangeProviderLogsPage(getIt.get<ExchangeProviderLogsViewModel>()));
getIt.registerFactory(() => StartTorPage(StartTorViewModel(),));
getIt.registerFactory(() => DEuroViewModel(
@@ -13,6 +13,7 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:hive/hive.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class ChainflipExchangeProvider extends ExchangeProvider {
ChainflipExchangeProvider({required this.tradesStore})
@@ -114,8 +115,41 @@ class ChainflipExchangeProvider extends ExchangeProvider {
final expectedAmountOut =
quoteResponse['egressAmountNative'] as String? ?? '0';
return _amountFromNative(expectedAmountOut, to) / amount;
} catch (e) {
final rate = _amountFromNative(expectedAmountOut, to) / amount;
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'quoteParams': quoteParams,
},
responseData: {
'expectedAmountOut': expectedAmountOut,
'rate': rate,
'quoteResponse': quoteResponse,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
printV(e.toString());
return 0.0;
}
@@ -164,6 +198,30 @@ class ChainflipExchangeProvider extends ExchangeProvider {
final id = '${swapResponse['issuedBlock']}-${swapResponse['network'].toString().toUpperCase()}-${swapResponse['channelId']}';
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'quoteParams': quoteParams,
'swapParams': swapParams,
},
responseData: {
'id': id,
'inputAddress': swapResponse['address'].toString(),
'estimatedPrice': estimatedPrice,
'minimumPrice': minimumPrice,
'swapResponse': swapResponse,
},
);
return Trade(
id: id,
from: request.fromCurrency,
@@ -178,7 +236,23 @@ class ChainflipExchangeProvider extends ExchangeProvider {
userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
isSendAll: isSendAll);
} catch (e) {
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: e,
stackTrace: s,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
},
);
printV(e.toString());
rethrow;
}
@@ -16,6 +16,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class ChangeNowExchangeProvider extends ExchangeProvider {
ChangeNowExchangeProvider({required SettingsStore settingsStore})
@@ -126,8 +127,43 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
if (rateId.isNotEmpty) _lastUsedRateId = rateId;
return isReverse ? (amount / fromAmount) : (toAmount / amount);
} catch (e) {
final rate = isReverse ? (amount / fromAmount) : (toAmount / amount);
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'type': type,
'flow': _getFlow(isFixedRateMode),
},
responseData: {
'fromAmount': fromAmount,
'toAmount': toAmount,
'rateId': rateId,
'rate': rate,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
printV(e.toString());
return 0.0;
}
@@ -13,6 +13,7 @@ import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class ExolixExchangeProvider extends ExchangeProvider {
ExolixExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -141,11 +142,62 @@ class ExolixExchangeProvider extends ExchangeProvider {
if (response.statusCode != 200) {
final message = responseJSON['message'] as String?;
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: Exception(message ?? 'Unknown error'),
stackTrace: StackTrace.current,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'params': params,
'url': uri.toString(),
},
);
throw Exception(message);
}
return responseJSON['rate'] as double;
} catch (e) {
final rate = responseJSON['rate'] as double;
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'params': params,
'url': uri.toString(),
},
responseData: {
'rate': rate,
'statusCode': response.statusCode,
'responseJSON': responseJSON,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
printV(e.toString());
return 0.0;
}
@@ -186,11 +238,50 @@ class ExolixExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final errors = responseJSON['errors'] as Map<String, String>;
final errorMessage = errors.values.join(', ');
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception(errorMessage),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
);
throw Exception(errorMessage);
}
if (response.statusCode != 200 && response.statusCode != 201)
if (response.statusCode != 200 && response.statusCode != 201) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception('Unexpected http status: ${response.statusCode}'),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
);
throw Exception('Unexpected http status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final id = responseJSON['id'] as String;
@@ -201,6 +292,34 @@ class ExolixExchangeProvider extends ExchangeProvider {
final amount = responseJSON['amount'].toString();
final receiveAmount = responseJSON['amountTo']?.toString();
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
responseData: {
'id': id,
'inputAddress': inputAddress,
'refundAddress': refundAddress,
'extraId': extraId,
'payoutAddress': payoutAddress,
'amount': amount,
'receiveAmount': receiveAmount,
'statusCode': response.statusCode,
'responseJSON': responseJSON,
},
);
return Trade(
id: id,
from: request.fromCurrency,
@@ -13,6 +13,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class LetsExchangeExchangeProvider extends ExchangeProvider {
LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -102,8 +103,45 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
if (amountToGet == 0.0) return 0.0;
return isFixedRateMode ? amount / amountToGet : amountToGet / amount;
} catch (e) {
final rate = isFixedRateMode ? amount / amountToGet : amountToGet / amount;
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'networkFrom': networkFrom,
'networkTo': networkTo,
'params': params,
},
responseData: {
'amountToGet': amountToGet,
'rate': rate,
'responseJSON': responseJSON,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'networkFrom': networkFrom,
'networkTo': networkTo,
},
);
printV(e.toString());
return 0.0;
}
@@ -164,6 +202,26 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
if (response.statusCode != 200) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception('LetsExchange create trade failed: ${response.body}'),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'networkFrom': networkFrom,
'networkTo': networkTo,
'tradeParams': tradeParams,
'url': uri.toString(),
},
);
throw Exception('LetsExchange create trade failed: ${response.body}');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -197,6 +255,40 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
toCurrency = CryptoCurrency.fromString(to);
}
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'networkFrom': networkFrom,
'networkTo': networkTo,
'tradeParams': tradeParams,
'url': uri.toString(),
},
responseData: {
'id': id,
'from': from,
'to': to,
'depositAddress': depositAddress,
'payoutAddress': payoutAddress,
'refundAddress': refundAddress,
'depositAmount': depositAmount,
'receiveAmount': receiveAmount,
'status': status,
'createdAt': createdAtString,
'expiredAt': expiredAtTimestamp,
'extraId': extraId,
'statusCode': response.statusCode,
},
);
return Trade(
id: id,
from: fromCurrency,
@@ -215,7 +307,25 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
isSendAll: isSendAll,
);
} catch (e) {
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: e,
stackTrace: s,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'networkFrom': networkFrom,
'networkTo': networkTo,
},
);
log(e.toString());
throw TradeNotCreatedException(description);
}
@@ -13,6 +13,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class SideShiftExchangeProvider extends ExchangeProvider {
SideShiftExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -145,15 +146,77 @@ class SideShiftExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error']['message'] as String;
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: Exception('SideShift Internal Server Error: $error'),
stackTrace: StackTrace.current,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'url': url,
},
);
throw Exception('SideShift Internal Server Error: $error');
}
if (response.statusCode != 200) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: Exception('Unexpected http status: ${response.statusCode}'),
stackTrace: StackTrace.current,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'url': url,
},
);
throw Exception('Unexpected http status: ${response.statusCode}');
}
return double.parse(responseJSON['rate'] as String);
} catch (e) {
final rate = double.parse(responseJSON['rate'] as String);
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'url': url,
},
responseData: {
'rate': rate,
'statusCode': response.statusCode,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
printV(e.toString());
return 0.00;
}
@@ -199,9 +262,47 @@ class SideShiftExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error']['message'] as String;
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: TradeNotCreatedException(description, description: error),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'url': url,
'body': body,
},
);
throw TradeNotCreatedException(description, description: error);
}
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: TradeNotCreatedException(description),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'url': url,
'body': body,
},
);
throw TradeNotCreatedException(description);
}
@@ -212,6 +313,31 @@ class SideShiftExchangeProvider extends ExchangeProvider {
final depositAmount = responseJSON['depositAmount'] as String?;
final depositMemo = responseJSON['depositMemo'] as String?;
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'url': url,
'body': body,
},
responseData: {
'id': id,
'inputAddress': inputAddress,
'settleAddress': settleAddress,
'depositAmount': depositAmount,
'depositMemo': depositMemo,
'statusCode': response.statusCode,
},
);
return Trade(
id: id,
provider: description,
@@ -13,6 +13,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class SimpleSwapExchangeProvider extends ExchangeProvider {
SimpleSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -108,11 +109,63 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
final response = await ProxyWrapper().get(clearnetUri: uri);
if (response.body == "null") return 0.00;
if (response.body == "null") {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: Exception('Null response body'),
stackTrace: StackTrace.current,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'params': params,
'url': uri.toString(),
},
);
return 0.00;
}
final data = json.decode(response.body) as String;
final rate = double.parse(data) / amount;
return double.parse(data) / amount;
} catch (_) {
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'params': params,
'url': uri.toString(),
},
responseData: {
'data': data,
'rate': rate,
'statusCode': response.statusCode,
'responseBody': response.body,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
return 0.00;
}
}
@@ -147,9 +200,47 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['message'] as String;
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: TradeNotCreatedException(description, description: error),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
);
throw TradeNotCreatedException(description, description: error);
}
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: TradeNotCreatedException(description),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
);
throw TradeNotCreatedException(description);
}
@@ -161,6 +252,33 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
final extraId = responseJSON['extra_id_from'] as String?;
final receiveAmount = responseJSON['amount_to'] as String?;
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
responseData: {
'id': id,
'inputAddress': inputAddress,
'payoutAddress': payoutAddress,
'settleAddress': settleAddress,
'extraId': extraId,
'receiveAmount': receiveAmount,
'statusCode': response.statusCode,
'responseJSON': responseJSON,
},
);
return Trade(
id: id,
provider: description,
@@ -12,6 +12,7 @@ import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class StealthExExchangeProvider extends ExchangeProvider {
StealthExExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -68,7 +69,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
headers: headers,
body: json.encode(body),
);
if (response.statusCode != 200) {
throw Exception('StealthEx fetch limits failed: ${response.body}');
}
@@ -89,14 +90,68 @@ class StealthExExchangeProvider extends ExchangeProvider {
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
final response = await getEstimatedExchangeAmount(
from: from, to: to, amount: amount, isFixedRateMode: isFixedRateMode);
final estimatedAmount = response['estimated_amount'] as double? ?? 0.0;
return estimatedAmount > 0.0
? isFixedRateMode
? amount / estimatedAmount
: estimatedAmount / amount
: 0.0;
try {
final response = await getEstimatedExchangeAmount(
from: from,
to: to,
amount: amount,
isFixedRateMode: isFixedRateMode,
);
final estimatedAmount = response['estimated_amount'] as double? ?? 0.0;
if (estimatedAmount <= 0.0) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: Exception('Invalid estimated amount: $estimatedAmount'),
stackTrace: StackTrace.current,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
return 0.0;
}
final rate = isFixedRateMode ? amount / estimatedAmount : estimatedAmount / amount;
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
responseData: {
'estimatedAmount': estimatedAmount,
'rate': rate,
'response': response,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
return 0.0;
}
}
@override
@@ -143,9 +198,26 @@ class StealthExExchangeProvider extends ExchangeProvider {
headers: headers,
body: json.encode(body),
);
if (response.statusCode != 201) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception('StealthEx create trade failed: ${response.body}'),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'rateId': rateId,
},
);
throw Exception('StealthEx create trade failed: ${response.body}');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -169,20 +241,51 @@ class StealthExExchangeProvider extends ExchangeProvider {
? DateTime.parse(validUntil).toLocal()
: DateTime.now().add(Duration(minutes: 5));
CryptoCurrency fromCurrency;
if (request.fromCurrency.tag != null && request.fromCurrency.title.toLowerCase() == from) {
fromCurrency = request.fromCurrency;
} else {
fromCurrency = CryptoCurrency.fromString(from);
}
fromCurrency = request.fromCurrency;
} else {
fromCurrency = CryptoCurrency.fromString(from);
}
CryptoCurrency toCurrency;
if (request.toCurrency.tag != null && request.toCurrency.title.toLowerCase() == to) {
toCurrency = request.toCurrency;
} else {
toCurrency = CryptoCurrency.fromString(to);
}
toCurrency = request.toCurrency;
} else {
toCurrency = CryptoCurrency.fromString(to);
}
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'rateId': rateId,
},
responseData: {
'id': id,
'from': from,
'to': to,
'depositAddress': depositAddress,
'payoutAddress': payoutAddress,
'refundAddress': refundAddress,
'depositAmount': depositAmount,
'receiveAmount': receiveAmount,
'status': status,
'createdAt': createdAtString,
'extraId': extraId,
'statusCode': response.statusCode,
'responseJSON': responseJSON,
},
);
return Trade(
id: id,
@@ -202,7 +305,23 @@ class StealthExExchangeProvider extends ExchangeProvider {
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
isSendAll: isSendAll,
);
} catch (e) {
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: e,
stackTrace: s,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
},
);
log(e.toString());
throw TradeNotCreatedException(description);
}
@@ -214,8 +333,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
final uri = Uri.parse('$_baseUrl$_exchangesPath/$id');
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
if (response.statusCode != 200) {
throw Exception('StealthEx fetch trade failed: ${response.body}');
}
@@ -272,7 +390,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
'estimation': isFixedRateMode ? 'reversed' : 'direct',
'rate': isFixedRateMode ? 'fixed' : 'floating',
'amount': amount,
'additional_fee_percent': _additionalFeePercent,
'additional_fee_percent': _additionalFeePercent,
};
try {
@@ -281,7 +399,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
headers: headers,
body: json.encode(body),
);
if (response.statusCode != 200) return {};
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final rate = responseJSON['rate'] as Map<String, dynamic>?;
@@ -13,6 +13,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class SwapTradeExchangeProvider extends ExchangeProvider {
SwapTradeExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -126,13 +127,64 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
final responseBody = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode != 200)
if (response.statusCode != 200) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: Exception('Unexpected http status: ${response.statusCode}'),
stackTrace: StackTrace.current,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'body': body,
'url': uri.toString(),
},
);
throw Exception('Unexpected http status: ${response.statusCode}');
}
final data = responseBody['data'] as Map<String, dynamic>;
double rate = double.parse(data['price'].toString());
return rate > 0 ? isFixedRateMode ? amount / rate : rate / amount : 0.0;
} catch (e) {
final calculatedRate = rate > 0 ? isFixedRateMode ? amount / rate : rate / amount : 0.0;
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'body': body,
'url': uri.toString(),
},
responseData: {
'rate': rate,
'calculatedRate': calculatedRate,
'statusCode': response.statusCode,
'responseBody': responseBody,
},
);
return calculatedRate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
printV("error fetching rate: ${e.toString()}");
return 0.0;
}
@@ -169,15 +221,78 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
if (response.statusCode == 400 || responseBody["success"] == false) {
final error = responseBody['errors'][0]['msg'] as String;
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: TradeNotCreatedException(description, description: error),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
);
throw TradeNotCreatedException(description, description: error);
}
if (response.statusCode != 200)
if (response.statusCode != 200) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception('Unexpected http status: ${response.statusCode}'),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
);
throw Exception('Unexpected http status: ${response.statusCode}');
}
final responseData = responseBody['data'] as Map<String, dynamic>;
final receiveAmount = responseData["amount_receive"]?.toString();
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'body': body,
'url': uri.toString(),
},
responseData: {
'id': responseData["order_id"] as String,
'inputAddress': responseData["server_address"] as String,
'receiveAmount': receiveAmount,
'statusCode': response.statusCode,
'responseBody': responseBody,
},
);
return Trade(
id: responseData["order_id"] as String,
inputAddress: responseData["server_address"] as String,
@@ -193,7 +308,23 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
);
} catch (e) {
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: e,
stackTrace: s,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
},
);
printV("error creating trade: ${e.toString()}");
throw TradeNotCreatedException(description, description: e.toString());
}
@@ -11,6 +11,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:hive/hive.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class ThorChainExchangeProvider extends ExchangeProvider {
ThorChainExchangeProvider({required this.tradesStore})
@@ -85,9 +86,41 @@ class ThorChainExchangeProvider extends ExchangeProvider {
final responseJSON = await _getSwapQuote(params);
final expectedAmountOut = responseJSON['expected_amount_out'] as String? ?? '0.0';
final rate = _thorChainAmountToDouble(expectedAmountOut) / amount;
return _thorChainAmountToDouble(expectedAmountOut) / amount;
} catch (e) {
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'params': params,
},
responseData: {
'expectedAmountOut': expectedAmountOut,
'rate': rate,
'responseJSON': responseJSON,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
printV(e.toString());
return 0.0;
}
@@ -144,6 +177,29 @@ class ThorChainExchangeProvider extends ExchangeProvider {
receiveAmount = _thorChainAmountToDouble(directAmountOutResponse).toString();
}
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'params': params,
},
responseData: {
'inputAddress': inputAddress,
'memo': memo,
'directAmountOutResponse': directAmountOutResponse,
'receiveAmount': receiveAmount,
'responseJSON': responseJSON,
},
);
return Trade(
id: '',
from: request.fromCurrency,
@@ -253,10 +309,30 @@ class ThorChainExchangeProvider extends ExchangeProvider {
final response = await ProxyWrapper().get(clearnetUri: uri);
if (response.statusCode != 200) {
ExchangeProviderLogger.logError(
provider: description,
function: '_getSwapQuote',
error: Exception('Unexpected HTTP status: ${response.statusCode}'),
stackTrace: StackTrace.current,
requestData: {
'params': params,
'url': uri.toString(),
},
);
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
if (response.body.contains('error')) {
ExchangeProviderLogger.logError(
provider: description,
function: '_getSwapQuote',
error: Exception('Unexpected response: ${response.body}'),
stackTrace: StackTrace.current,
requestData: {
'params': params,
'url': uri.toString(),
},
);
throw Exception('Unexpected response: ${response.body}');
}
@@ -12,6 +12,7 @@ import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class TrocadorExchangeProvider extends ExchangeProvider {
TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}})
@@ -156,13 +157,66 @@ class TrocadorExchangeProvider extends ExchangeProvider {
.toList();
if (_provider.isEmpty) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: Exception('No enabled providers found for the selected trade.'),
stackTrace: StackTrace.current,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'params': params,
'url': uri.toString(),
},
);
throw Exception('No enabled providers found for the selected trade.');
}
if (rateId.isNotEmpty) _lastUsedRateId = rateId;
return isReceiveAmount ? (amount / fromAmount) : (toAmount / amount);
} catch (e) {
final rate = isReceiveAmount ? (amount / fromAmount) : (toAmount / amount);
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
'params': params,
'url': uri.toString(),
},
responseData: {
'fromAmount': fromAmount,
'toAmount': toAmount,
'rate': rate,
'rateId': rateId,
'provider': _provider.first,
'statusCode': response.statusCode,
'responseJSON': responseJSON,
},
);
return rate;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
printV(e.toString());
return 0.0;
}
@@ -201,6 +255,23 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
if (_provider.isEmpty) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception('No available provider is enabled'),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'params': params,
},
);
throw Exception('No available provider is enabled');
}
@@ -214,11 +285,50 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error'] as String;
final message = responseJSON['message'] as String;
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception('${error}\n$message'),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'params': params,
'url': uri.toString(),
},
);
throw Exception('${error}\n$message');
}
if (response.statusCode != 200)
if (response.statusCode != 200) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception('Unexpected http status: ${response.statusCode}'),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'params': params,
'url': uri.toString(),
},
);
throw Exception('Unexpected http status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final id = responseJSON['trade_id'] as String;
@@ -234,6 +344,39 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final receiveAmount = responseJSON['amount_to']?.toString();
final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'params': params,
'url': uri.toString(),
},
responseData: {
'id': id,
'inputAddress': inputAddress,
'refundAddress': refundAddress,
'status': status,
'payoutAddress': payoutAddress,
'date': date,
'password': password,
'providerId': providerId,
'providerName': providerName,
'amount': amount,
'receiveAmount': receiveAmount,
'addressProviderMemo': addressProviderMemo,
'statusCode': response.statusCode,
'responseJSON': responseJSON,
},
);
return Trade(
id: id,
from: request.fromCurrency,
@@ -11,6 +11,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
class XOSwapExchangeProvider extends ExchangeProvider {
XOSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported));
@@ -150,8 +151,24 @@ class XOSwapExchangeProvider extends ExchangeProvider {
}) async {
try {
final rates = await getRatesForPair(from: from, to: to);
if (rates.isEmpty) return 0;
if (rates.isEmpty) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: Exception('No rates found for $from to $to'),
stackTrace: StackTrace.current,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
return 0;
}
double result;
if (!isFixedRateMode) {
double bestOutput = 0.0;
for (var rate in rates) {
@@ -166,7 +183,7 @@ class XOSwapExchangeProvider extends ExchangeProvider {
}
}
}
return bestOutput > 0 ? (bestOutput / amount) : 0;
result = bestOutput > 0 ? (bestOutput / amount) : 0;
} else {
double bestInput = double.infinity;
for (var rate in rates) {
@@ -181,9 +198,41 @@ class XOSwapExchangeProvider extends ExchangeProvider {
}
}
}
return bestInput < double.infinity ? amount / bestInput : 0;
result = bestInput < double.infinity ? amount / bestInput : 0;
}
} catch (e) {
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'fetchRate',
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
responseData: {
'result': result,
'ratesCount': rates.length,
'rates': rates,
},
);
return result;
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'fetchRate',
error: e,
stackTrace: s,
requestData: {
'from': from.title,
'to': to.title,
'amount': amount,
'isFixedRateMode': isFixedRateMode,
'isReceiveAmount': isReceiveAmount,
},
);
printV(e.toString());
return 0;
}
@@ -202,6 +251,24 @@ class XOSwapExchangeProvider extends ExchangeProvider {
final curTo = await _getAssets(request.toCurrency);
if (curFrom == null || curTo == null) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: TradeNotCreatedException(description),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'curFrom': curFrom,
'curTo': curTo,
},
);
throw TradeNotCreatedException(description);
}
@@ -225,6 +292,26 @@ class XOSwapExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error'] ?? 'Unknown error';
final message = responseJSON['message'] ?? '';
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: Exception('$error\n$message'),
stackTrace: StackTrace.current,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'payload': payload,
'url': uri.toString(),
},
);
throw Exception('$error\n$message');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -245,6 +332,36 @@ class XOSwapExchangeProvider extends ExchangeProvider {
final createdAt = DateTime.parse(createdAtString).toLocal();
ExchangeProviderLogger.logSuccess(
provider: description,
function: 'createTrade',
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
'payload': payload,
'url': uri.toString(),
},
responseData: {
'orderId': orderId,
'depositAddress': depositAddress,
'payoutAddress': payoutAddress,
'refundAddress': refundAddress,
'depositAmount': depositAmount,
'receiveAmount': receiveAmount,
'status': status,
'createdAt': createdAtString,
'extraId': extraId,
'statusCode': response.statusCode,
'responseJSON': responseJSON,
},
);
return Trade(
id: orderId,
from: from,
@@ -262,7 +379,23 @@ class XOSwapExchangeProvider extends ExchangeProvider {
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
isSendAll: isSendAll,
);
} catch (e) {
} catch (e, s) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
error: e,
stackTrace: s,
requestData: {
'from': request.fromCurrency.title,
'to': request.toCurrency.title,
'fromAmount': request.fromAmount,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'refundAddress': request.refundAddress,
'isFixedRateMode': isFixedRateMode,
'isSendAll': isSendAll,
},
);
printV(e.toString());
throw TradeNotCreatedException(description);
}
+6
View File
@@ -34,6 +34,7 @@ import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
@@ -924,6 +925,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => getIt.get<DevNetworkRequests>(),
);
case Routes.devExchangeProviderLogs:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<DevExchangeProviderLogsPage>(),
);
case Routes.devMoneroCallProfiler:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<DevMoneroCallProfilerPage>(),
+1
View File
@@ -123,6 +123,7 @@ class Routes {
static const devBackgroundSyncLogs = '/dev/background_sync_logs';
static const devSocketHealthLogs = '/dev/socket_health_logs';
static const devNetworkRequests = '/dev/network_requests';
static const devExchangeProviderLogs = '/dev/exchange_provider_logs';
static const signPage = '/sign_page';
static const connectDevices = '/device/connect';
@@ -0,0 +1,562 @@
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/view_model/dev/exchange_provider_logs_view_model.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:intl/intl.dart';
class DevExchangeProviderLogsPage extends BasePage {
final ExchangeProviderLogsViewModel viewModel;
DevExchangeProviderLogsPage(this.viewModel) {
viewModel.loadLogs();
}
@override
String? get title => "[dev] exchange provider logs";
@override
Widget? trailing(BuildContext context) {
return IconButton(
icon: Icon(Icons.refresh),
onPressed: () => viewModel.refreshLogs(),
);
}
@override
Widget body(BuildContext context) {
return Observer(
builder: (_) {
if (viewModel.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (viewModel.error != null) {
return Center(child: Text("Error: ${viewModel.error}"));
}
if (viewModel.logs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("No exchange provider logs available"),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => viewModel.loadLogs(),
child: Text("Load Logs"),
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_StatsCard(viewModel),
_LogsHeader(viewModel),
Expanded(
child: _LogsList(viewModel),
),
_ActionButtons(viewModel),
],
);
},
);
}
}
class _StatsCard extends StatefulWidget {
final ExchangeProviderLogsViewModel viewModel;
const _StatsCard(this.viewModel);
@override
State<_StatsCard> createState() => _StatsCardState();
}
class _StatsCardState extends State<_StatsCard> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(16),
child: Column(
children: [
ListTile(
title: Text(
"Stats",
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
),
),
trailing: Icon(
_isExpanded ? Icons.expand_less : Icons.expand_more,
),
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
),
if (_isExpanded) ...[
Observer(
builder: (_) => Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_StatItem("Total", widget.viewModel.totalLogs.toString()),
_StatItem("Success", widget.viewModel.successLogs.toString()),
_StatItem("Errors", widget.viewModel.errorLogs.toString()),
],
),
SizedBox(height: 16),
Text(
"Logs by Provider:",
style: Theme.of(context).textTheme.titleMedium,
),
SizedBox(height: 8),
...widget.viewModel.logsByProvider.entries.map(
(entry) => Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(entry.key.title),
Text(entry.value.toString()),
],
),
),
),
],
),
),
),
],
],
),
);
}
}
class _LogsHeader extends StatelessWidget {
final ExchangeProviderLogsViewModel viewModel;
const _LogsHeader(this.viewModel);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Logs",
style: Theme.of(context).textTheme.titleMedium,
),
Observer(
builder: (_) => PopupMenuButton<LogFilter>(
tooltip: "Filter logs",
onSelected: (LogFilter filter) {
viewModel.setFilter(filter);
},
itemBuilder: (context) => [
PopupMenuItem(
value: LogFilter.all,
child: Row(
children: [
Icon(Icons.list, size: 18),
SizedBox(width: 8),
Text("All Logs"),
],
),
),
PopupMenuItem(
value: LogFilter.success,
child: Row(
children: [
Icon(Icons.check_circle, size: 18, color: Colors.green),
SizedBox(width: 8),
Text("Success Only"),
],
),
),
PopupMenuItem(
value: LogFilter.error,
child: Row(
children: [
Icon(Icons.error, size: 18, color: Colors.red),
SizedBox(width: 8),
Text("Errors Only"),
],
),
),
],
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.filter_list, size: 16),
SizedBox(width: 4),
Text(_getFilterText(viewModel.currentFilter)),
Icon(Icons.arrow_drop_down, size: 16),
],
),
),
),
),
],
),
);
}
String _getFilterText(LogFilter filter) {
switch (filter) {
case LogFilter.all:
return "All";
case LogFilter.success:
return "Success";
case LogFilter.error:
return "Errors";
}
}
}
class _StatItem extends StatelessWidget {
final String label;
final String value;
const _StatItem(this.label, this.value);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
value,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(label),
],
);
}
}
class _LogsList extends StatelessWidget {
final ExchangeProviderLogsViewModel viewModel;
const _LogsList(this.viewModel);
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) => ListView.builder(
itemCount: viewModel.filteredLogs.length,
itemBuilder: (context, index) {
final log = viewModel.filteredLogs[index];
return _LogEntryCard(log);
},
),
);
}
}
class _LogEntryCard extends StatelessWidget {
final ExchangeProviderLogEntry log;
const _LogEntryCard(this.log);
void _copyToClipboard(BuildContext context, String text) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Copied to clipboard"),
duration: Duration(seconds: 2),
),
);
}
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ExpansionTile(
title: Row(
children: [
Icon(
log.isSuccess ? Icons.check_circle : Icons.error,
color: log.isSuccess ? Colors.green : Colors.red,
size: 20,
),
SizedBox(width: 8),
Expanded(
child: Text(
"${log.provider.title} - ${log.function}",
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
subtitle: Text(
dateFormat.format(log.timestamp),
style: Theme.of(context).textTheme.bodySmall,
),
children: [
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (log.error != null) ...[
Text(
"Error:",
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.red,
),
),
SizedBox(height: 4),
Container(
width: double.infinity,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.red.shade200),
),
child: GestureDetector(
onTap: () => _copyToClipboard(context, log.error!),
child: Text(
log.error!,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Colors.black,
),
),
),
),
SizedBox(height: 16),
],
if (log.stackTrace != null) ...[
Text(
"Stack Trace:",
style: Theme.of(context).textTheme.titleSmall,
),
SizedBox(height: 4),
Container(
width: double.infinity,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.grey.shade300),
),
child: GestureDetector(
onTap: () => _copyToClipboard(context, log.stackTrace!),
child: Text(
log.stackTrace!,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: Colors.black,
),
),
),
),
SizedBox(height: 16),
],
if (log.callStack != null) ...[
Text(
"Call Stack:",
style: Theme.of(context).textTheme.titleSmall,
),
SizedBox(height: 4),
Container(
width: double.infinity,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.blue.shade200),
),
child: GestureDetector(
onTap: () => _copyToClipboard(context, log.callStack!),
child: Text(
log.callStack!,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: Colors.black,
),
),
),
),
SizedBox(height: 16),
],
if (log.requestData != null) ...[
Text(
"Request Data:",
style: Theme.of(context).textTheme.titleSmall,
),
SizedBox(height: 4),
Container(
width: double.infinity,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.green.shade200),
),
child: GestureDetector(
onTap: () => _copyToClipboard(context, log.requestData.toString()),
child: Text(
log.requestData.toString(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: Colors.black,
),
),
),
),
SizedBox(height: 16),
],
if (log.responseData != null) ...[
Text(
"Response Data:",
style: Theme.of(context).textTheme.titleSmall,
),
SizedBox(height: 4),
Container(
width: double.infinity,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.orange.shade200),
),
child: GestureDetector(
onTap: () => _copyToClipboard(context, log.responseData.toString()),
child: Text(
log.responseData.toString(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: Colors.black,
),
),
),
),
],
],
),
),
],
),
);
}
}
class _ActionButtons extends StatelessWidget {
final ExchangeProviderLogsViewModel viewModel;
const _ActionButtons(this.viewModel);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16),
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () => _copyLogsAsText(context),
icon: Icon(Icons.copy, size: 18),
label: Text("Copy Text"),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
ElevatedButton.icon(
onPressed: () => _copyLogsAsJson(context),
icon: Icon(Icons.code, size: 18),
label: Text("Copy JSON"),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
ElevatedButton.icon(
onPressed: () => _showClearDialog(context),
icon: Icon(Icons.clear, size: 18),
label: Text("Clear"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
],
),
);
}
void _copyLogsAsText(BuildContext context) {
final logsText = viewModel.getLogsAsText();
Clipboard.setData(ClipboardData(text: logsText));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Logs copied to clipboard")),
);
}
void _copyLogsAsJson(BuildContext context) {
final logsJson = viewModel.getLogsAsJson();
Clipboard.setData(ClipboardData(text: logsJson));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Logs JSON copied to clipboard")),
);
}
void _showClearDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Clear Exchange Provider Logs"),
content: Text(
"Are you sure you want to clear all exchange provider logs? This action cannot be undone."),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text("Cancel"),
),
TextButton(
onPressed: () {
viewModel.clearLogs();
Navigator.of(context).pop();
},
child: Text("Clear", style: TextStyle(color: Colors.red)),
),
],
),
);
}
}
@@ -73,7 +73,8 @@ class ExchangeTradeExternalSendPage extends BasePage {
Routes.fullscreenQR,
arguments: QrViewData(
embeddedImagePath: exchangeTradeViewModel.qrImage,
data: exchangeTradeViewModel.trade.inputAddress ??
data: exchangeTradeViewModel.paymentUri?.toString() ??
exchangeTradeViewModel.trade.inputAddress ??
fetchingLabel,
),
);
@@ -92,7 +93,8 @@ class ExchangeTradeExternalSendPage extends BasePage {
),
),
child: QrImage(
data: exchangeTradeViewModel.trade.inputAddress ??
data: exchangeTradeViewModel.paymentUri?.toString() ??
exchangeTradeViewModel.trade.inputAddress ??
fetchingLabel,
embeddedImagePath: exchangeTradeViewModel.qrImage,
size: 230,
@@ -1,3 +1,4 @@
import 'package:cake_wallet/core/payment_uris.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/routes.dart';
+8 -4
View File
@@ -121,8 +121,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) {
if (mounted) {
final separator = initialPaymentRequest!.scheme.isNotEmpty ? ":" : "";
final uri = initialPaymentRequest!.scheme + separator + initialPaymentRequest!.address;
final prefix = initialPaymentRequest!.scheme.isNotEmpty ? "${initialPaymentRequest!.scheme}:" : "";
final amount = initialPaymentRequest!.amount.isNotEmpty ? "?amount=${initialPaymentRequest!.amount}" : "";
final uri = prefix + initialPaymentRequest!.address + amount;
_handlePaymentFlow(uri, initialPaymentRequest!);
}
},
@@ -145,6 +146,10 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
try {
final result = await paymentViewModel.processAddress(uri);
if (paymentRequest.contractAddress != null) {
await sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!);
}
switch (result.type) {
case PaymentFlowType.singleWallet:
case PaymentFlowType.multipleWallets:
@@ -256,8 +261,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
}
});
await Future.delayed(const Duration(seconds: 2));
if (loadingBottomSheetContext != null &&
loadingBottomSheetContext!.mounted) {
if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
Navigator.of(loadingBottomSheetContext!).pop();
}
_applyPaymentRequest(paymentRequest);
@@ -111,6 +111,12 @@ class OtherSettingsPage extends BasePage {
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devNetworkRequests),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] exchange provider logs',
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devExchangeProviderLogs),
),
Spacer(),
SettingsVersionCell(
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
+189
View File
@@ -0,0 +1,189 @@
import 'dart:convert';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
class ExchangeProviderLogEntry {
final DateTime timestamp;
final ExchangeProviderDescription provider;
final String function;
final String? error;
final String? stackTrace;
final String? callStack;
final Map<String, dynamic>? requestData;
final Map<String, dynamic>? responseData;
final bool isSuccess;
ExchangeProviderLogEntry({
required this.timestamp,
required this.provider,
required this.function,
this.error,
this.stackTrace,
this.callStack,
this.requestData,
this.responseData,
required this.isSuccess,
});
String toLogString() {
final buffer = StringBuffer();
buffer.writeln('Provider: ${provider.title}');
buffer.writeln('Function: $function');
buffer.writeln('Timestamp: ${timestamp.toIso8601String()}');
buffer.writeln('Success: $isSuccess');
if (error != null) {
buffer.writeln('Error: $error');
}
if (stackTrace != null) {
buffer.writeln('StackTrace: $stackTrace');
}
if (callStack != null) {
buffer.writeln('CallStack: $callStack');
}
if (requestData != null) {
buffer.writeln('Request: ${json.encode(requestData)}');
}
if (responseData != null) {
buffer.writeln('Response: ${json.encode(responseData)}');
}
buffer.writeln('---');
return buffer.toString();
}
Map<String, dynamic> toJson() {
return {
'timestamp': timestamp.toIso8601String(),
'provider': provider.title,
'function': function,
'error': error,
'stackTrace': stackTrace,
'callStack': callStack,
'requestData': requestData,
'responseData': responseData,
'isSuccess': isSuccess,
};
}
factory ExchangeProviderLogEntry.fromJson(Map<String, dynamic> json) {
final allProviders = [
ExchangeProviderDescription.xmrto,
ExchangeProviderDescription.changeNow,
ExchangeProviderDescription.morphToken,
ExchangeProviderDescription.sideShift,
ExchangeProviderDescription.simpleSwap,
ExchangeProviderDescription.trocador,
ExchangeProviderDescription.exolix,
ExchangeProviderDescription.all,
ExchangeProviderDescription.thorChain,
ExchangeProviderDescription.swapTrade,
ExchangeProviderDescription.letsExchange,
ExchangeProviderDescription.stealthEx,
ExchangeProviderDescription.chainflip,
ExchangeProviderDescription.xoSwap,
];
return ExchangeProviderLogEntry(
timestamp: DateTime.parse(json['timestamp'] as String),
provider: allProviders.firstWhere(
(p) => p.title == json['provider'] as String,
orElse: () => ExchangeProviderDescription.changeNow,
),
function: json['function'] as String,
error: json['error'] as String?,
stackTrace: json['stackTrace'] as String?,
callStack: json['callStack'] as String?,
requestData: json['requestData'] as Map<String, dynamic>?,
responseData: json['responseData'] as Map<String, dynamic>?,
isSuccess: json['isSuccess'] as bool,
);
}
}
class ExchangeProviderLogger {
static final List<ExchangeProviderLogEntry> _logs = [];
static const int maxLogs = 100;
static List<ExchangeProviderLogEntry> get logs => List.unmodifiable(_logs);
static void logSuccess({
required ExchangeProviderDescription provider,
required String function,
Map<String, dynamic>? requestData,
Map<String, dynamic>? responseData,
String? callStack,
}) {
final entry = ExchangeProviderLogEntry(
timestamp: DateTime.now(),
provider: provider,
function: function,
requestData: requestData,
responseData: responseData,
callStack: callStack,
isSuccess: true,
);
_addLog(entry);
}
static void logError({
required ExchangeProviderDescription provider,
required String function,
required dynamic error,
StackTrace? stackTrace,
Map<String, dynamic>? requestData,
String? callStack,
}) {
final entry = ExchangeProviderLogEntry(
timestamp: DateTime.now(),
provider: provider,
function: function,
error: error.toString(),
stackTrace: stackTrace?.toString(),
requestData: requestData,
callStack: callStack,
isSuccess: false,
);
_addLog(entry);
}
static void _addLog(ExchangeProviderLogEntry entry) {
_logs.insert(0, entry);
if (_logs.length > maxLogs * 2) {
final excessCount = _logs.length - maxLogs;
_logs.removeRange(0, excessCount);
}
}
static void clearLogs() {
_logs.clear();
}
static String getLogsAsText() {
if (_logs.isEmpty) return 'No exchange provider logs available';
final buffer = StringBuffer();
buffer.writeln('Exchange Provider Logs');
buffer.writeln('Generated: ${DateTime.now().toIso8601String()}');
buffer.writeln('Total logs: ${_logs.length}');
buffer.writeln('Success logs: ${_logs.where((log) => log.isSuccess).length}');
buffer.writeln('Error logs: ${_logs.where((log) => !log.isSuccess).length}');
buffer.writeln('');
for (final log in _logs) {
buffer.writeln(log.toLogString());
}
return buffer.toString();
}
static String getLogsAsJson() {
return json.encode(_logs.map((log) => log.toJson()).toList());
}
}
+5 -55
View File
@@ -1,10 +1,9 @@
import 'package:cake_wallet/core/payment_uris.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cw_core/format_fixed.dart';
class PaymentRequest {
PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
{this.callbackUrl, this.callbackMessage});
{this.callbackUrl, this.callbackMessage, this.contractAddress});
factory PaymentRequest.fromUri(Uri? uri) {
var address = "";
@@ -15,6 +14,7 @@ class PaymentRequest {
String? callbackUrl;
String? callbackMessage;
String? pjUri;
String? contractAddress;
if (uri != null) {
if (uri.queryParameters['pj'] != null) {
@@ -34,6 +34,7 @@ class PaymentRequest {
address = paymentUri.address;
amount = paymentUri.amount;
contractAddress = paymentUri.contractAddress;
}
}
@@ -41,8 +42,6 @@ class PaymentRequest {
scheme = walletType ?? "nano";
}
if (nano != null) {
if (amount.isNotEmpty) {
if (address.contains("nano")) {
@@ -61,6 +60,7 @@ class PaymentRequest {
pjUri,
callbackUrl: callbackUrl,
callbackMessage: callbackMessage,
contractAddress: contractAddress,
);
}
@@ -71,55 +71,5 @@ class PaymentRequest {
final String? pjUri;
final String? callbackUrl;
final String? callbackMessage;
}
class ERC681URI extends PaymentURI {
final int chainId;
final String? contractAddress;
ERC681URI({
required this.chainId,
required super.address,
required super.amount,
required this.contractAddress,
});
factory ERC681URI.fromUri(Uri uri) {
final (isContract, targetAddress) = _getTargetAddress(uri.path);
final chainId = _getChainID(uri.path);
final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
final amount = isContract
? uri.queryParameters["uint256"]
: uri.queryParameters["value"];
var formatedAmount = "";
if (amount != null) {
formatedAmount = formatFixed(BigInt.parse(amount), 18);
} else {
formatedAmount = uri.queryParameters["amount"] ?? "";
}
return ERC681URI(
chainId: chainId,
address: address,
amount: formatedAmount,
contractAddress: isContract ? targetAddress : null,
);
}
static int _getChainID(String path) {
return int.parse(RegExp(
r'@\d*',
).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
"1");
}
static (bool, String) _getTargetAddress(String path) {
final targetAddress = RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false)
.firstMatch(path)!
.group(0)!;
return (path.contains("/"), targetAddress);
}
}
+226
View File
@@ -0,0 +1,226 @@
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/tron_token.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
class TokenUtilities {
static Future<List<Erc20Token>> loadAllUniqueEvmTokens(
Box<WalletInfo> walletInfoSource,
) async {
final evmWallets = walletInfoSource.values.where(
(w) => w.type == WalletType.ethereum || w.type == WalletType.polygon,
);
final seen = <String>{};
final unique = <Erc20Token>[];
for (final wallet in evmWallets) {
final chain = wallet.type == WalletType.ethereum ? 'ETH' : 'POL';
final box = await _openEvmTokensBoxFor(wallet);
for (final t in box.values.where((t) => t.enabled)) {
final key = '$chain|${t.contractAddress.toLowerCase()}';
if (seen.add(key)) {
unique.add(t);
}
}
}
return unique;
}
static Future<List<SPLToken>> loadAllUniqueSolTokens(
Box<WalletInfo> walletInfoSource,
) async {
final solWallets = walletInfoSource.values.where(
(w) => w.type == WalletType.solana,
);
final tokens = <SPLToken>[];
for (final wallet in solWallets) {
final box = await _openSolTokensBoxFor(wallet);
tokens.addAll(box.values.where((t) => t.enabled));
}
final seen = <String>{};
final unique = <SPLToken>[];
for (final token in tokens) {
final key = token.mintAddress.toLowerCase();
if (seen.add(key)) unique.add(token);
}
return unique;
}
static Future<List<TronToken>> loadAllUniqueTronTokens(
Box<WalletInfo> walletInfoSource,
) async {
final tronWallets = walletInfoSource.values.where(
(w) => w.type == WalletType.tron,
);
final seen = <String>{};
final unique = <TronToken>[];
for (final wallet in tronWallets) {
final box = await _openTronTokensBoxFor(wallet);
for (final t in box.values.where((t) => t.enabled)) {
final key = t.contractAddress.toLowerCase();
if (seen.add(key)) unique.add(t);
}
}
return unique;
}
/// Finds a token by address across wallets depending on [walletType]
/// - EVM chains: match by contractAddress
/// - Solana: match by mintAddress
/// - Tron: match by contractAddress
static Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required Box<WalletInfo> walletInfoSource,
required String address,
}) async {
final lower = address.toLowerCase();
switch (walletType) {
case WalletType.ethereum:
case WalletType.polygon:
final tokens = await loadAllUniqueEvmTokens(walletInfoSource);
for (final t in tokens) {
if (t.contractAddress.toLowerCase() == lower) return t;
}
return null;
case WalletType.solana:
final solTokens = await loadAllUniqueSolTokens(walletInfoSource);
for (final t in solTokens) {
if (t.mintAddress.toLowerCase() == lower) return t;
}
return null;
case WalletType.tron:
final tronTokens = await loadAllUniqueTronTokens(walletInfoSource);
for (final t in tronTokens) {
if (t.contractAddress.toLowerCase() == lower) return t;
}
return null;
default:
return null;
}
}
static Future<Box<Erc20Token>> _openEvmTokensBoxFor(
WalletInfo walletInfo,
) async {
final walletKey = walletInfo.name.replaceAll(' ', '_');
final boxName = switch (walletInfo.type) {
WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}',
WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}',
_ => '${walletKey}_${Erc20Token.ethereumBoxName}',
};
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<Erc20Token>(boxName);
}
return CakeHive.openBox<Erc20Token>(boxName);
}
static Future<Box<SPLToken>> _openSolTokensBoxFor(WalletInfo wallet) async {
final boxName = '${wallet.name.replaceAll(' ', '_')}_${SPLToken.boxName}';
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<SPLToken>(boxName);
}
return CakeHive.openBox<SPLToken>(boxName);
}
static Future<Box<TronToken>> _openTronTokensBoxFor(
WalletInfo walletInfo,
) async {
final boxName = '${walletInfo.name.replaceAll(' ', '_')}_${TronToken.boxName}';
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<TronToken>(boxName);
}
return CakeHive.openBox<TronToken>(boxName);
}
static Erc20Token? findErc20Token(CryptoCurrency currency, WalletBase wallet) {
if (currency is Erc20Token) return currency;
// More of a fallback for us
for (final balanceCurrency in wallet.balance.keys) {
if (balanceCurrency is Erc20Token && _matchesToken(balanceCurrency, currency)) {
return balanceCurrency;
}
}
return null;
}
static bool isNativeToken(CryptoCurrency currency) {
final title = currency.title.toLowerCase();
final tag = currency.tag?.toLowerCase();
return title == 'eth' ||
title == 'ethereum' ||
title == 'matic' ||
title == 'polygon' ||
title == 'bnb' ||
title == 'bsc' ||
title == 'avax' ||
title == 'avalanche' ||
tag == 'polygon' ||
tag == 'bsc' ||
tag == 'avalanche';
}
static int getChainId(CryptoCurrency currency) {
final title = currency.title.toLowerCase();
final tag = currency.tag?.toLowerCase();
// Polygon
if (title == 'polygon' || title == 'matic' || tag == 'polygon') {
return 137;
}
// BSC (Binance Smart Chain)
if (title == 'bsc' || title == 'bnb' || tag == 'bsc') {
return 56;
}
// Avalanche C-Chain
if (title == 'avalanche' || title == 'avax' || tag == 'avalanche') {
return 43114;
}
// Arbitrum One
if (title == 'arbitrum' || title == 'arb' || tag == 'arbitrum') {
return 42161;
}
// Optimism
if (title == 'optimism' || title == 'op' || tag == 'optimism') {
return 10;
}
// Base
if (title == 'base' || tag == 'base') {
return 8453;
}
// Fantom Opera
if (title == 'fantom' || title == 'ftm' || tag == 'fantom') {
return 250;
}
// Default to Ethereum mainnet
return 1;
}
/// Checks if two currencies match (by title and tag)
static bool _matchesToken(Erc20Token token, CryptoCurrency currency) {
return token.title.toLowerCase() == currency.title.toLowerCase() &&
(token.tag?.toLowerCase() == currency.tag?.toLowerCase() ||
(token.tag == null && currency.tag == null));
}
}
@@ -0,0 +1,84 @@
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:mobx/mobx.dart';
part 'exchange_provider_logs_view_model.g.dart';
enum LogFilter { all, success, error }
class ExchangeProviderLogsViewModel = ExchangeProviderLogsViewModelBase with _$ExchangeProviderLogsViewModel;
abstract class ExchangeProviderLogsViewModelBase with Store {
@observable
bool isLoading = false;
@observable
String? error;
@observable
ObservableList<ExchangeProviderLogEntry> logs = ObservableList<ExchangeProviderLogEntry>();
@observable
LogFilter currentFilter = LogFilter.all;
@computed
ObservableList<ExchangeProviderLogEntry> get filteredLogs {
switch (currentFilter) {
case LogFilter.all:
return logs;
case LogFilter.success:
return ObservableList.of(logs.where((log) => log.isSuccess).toList());
case LogFilter.error:
return ObservableList.of(logs.where((log) => !log.isSuccess).toList());
}
}
@computed
int get totalLogs => logs.length;
@computed
int get successLogs => logs.where((log) => log.isSuccess).length;
@computed
int get errorLogs => logs.where((log) => !log.isSuccess).length;
@computed
Map<ExchangeProviderDescription, int> get logsByProvider {
final Map<ExchangeProviderDescription, int> counts = {};
for (final log in logs) {
counts[log.provider] = (counts[log.provider] ?? 0) + 1;
}
return counts;
}
String getLogsAsText() => ExchangeProviderLogger.getLogsAsText();
String getLogsAsJson() => ExchangeProviderLogger.getLogsAsJson();
@action
void loadLogs() {
isLoading = true;
error = null;
try {
logs.clear();
logs.addAll(ExchangeProviderLogger.logs);
} catch (e) {
error = e.toString();
} finally {
isLoading = false;
}
}
@action
void clearLogs() {
ExchangeProviderLogger.clearLogs();
logs.clear();
}
@action
void refreshLogs() => loadLogs();
@action
void setFilter(LogFilter filter) => currentFilter = filter;
}
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/core/payment_uris.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
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'
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/utils/qr_util.dart';
import 'package:cake_wallet/utils/token_utilities.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
@@ -317,6 +320,79 @@ abstract class ExchangeTradeViewModelBase with Store {
_isTronToken();
}
PaymentURI? get paymentUri {
final inputAddress = trade.inputAddress;
final amount = trade.amount;
final fromCurrency = trade.from ?? trade.userCurrencyFrom;
if (inputAddress == null || inputAddress.isEmpty || fromCurrency == null) {
return null;
}
switch (wallet.type) {
case WalletType.bitcoin:
return BitcoinURI(amount: amount, address: inputAddress);
case WalletType.litecoin:
return LitecoinURI(amount: amount, address: inputAddress);
case WalletType.bitcoinCash:
return BitcoinCashURI(amount: amount, address: inputAddress);
case WalletType.dogecoin:
return DogeURI(amount: amount, address: inputAddress);
case WalletType.ethereum:
return _createERC681URI(fromCurrency, inputAddress, amount);
// TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling)
case WalletType.polygon:
return PolygonURI(amount: amount, address: inputAddress);
case WalletType.solana:
return SolanaURI(amount: amount, address: inputAddress);
case WalletType.tron:
return TronURI(amount: amount, address: inputAddress);
case WalletType.monero:
return MoneroURI(amount: amount, address: inputAddress);
case WalletType.wownero:
return WowneroURI(amount: amount, address: inputAddress);
case WalletType.zano:
return ZanoURI(amount: amount, address: inputAddress);
case WalletType.decred:
return DecredURI(amount: amount, address: inputAddress);
case WalletType.haven:
return HavenURI(amount: amount, address: inputAddress);
case WalletType.nano:
return NanoURI(amount: amount, address: inputAddress);
default:
return null;
}
}
@action
PaymentURI? _createERC681URI(CryptoCurrency currency, String address, String amount) {
final chainId = TokenUtilities.getChainId(currency);
final isNativeToken = TokenUtilities.isNativeToken(currency);
if (isNativeToken) {
return ERC681URI(
chainId: chainId,
address: address,
amount: amount,
contractAddress: null,
);
} else {
if (wallet.type == WalletType.polygon || wallet.type == WalletType.ethereum) {
final erc20Token = TokenUtilities.findErc20Token(currency, wallet);
if (erc20Token != null) {
return ERC681URI(
chainId: chainId,
address: address,
amount: amount,
contractAddress: erc20Token.contractAddress,
);
}
}
return null;
}
}
@computed
String get qrImage => getQrImage(wallet.type);
}
+42 -104
View File
@@ -39,10 +39,10 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/token_utilities.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/spl_token.dart';
@@ -142,7 +142,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
});
bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => calculateBestRate());
bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) {
if (tradeState is! TradeIsCreating) {
calculateBestRate();
}
});
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
depositAmount = '';
@@ -536,19 +540,27 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
),
),
);
_sortedAvailableProviders.clear();
// We'll use a new SplayTreeMap to avoid concurrent modification issues
final newSortedProviders =
SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a));
for (int i = 0; i < result.length; i++) {
if (result[i] != 0) {
/// add this provider as its valid for this trade
try {
_sortedAvailableProviders[result[i]] = _providers[i];
newSortedProviders[result[i]] = _providers[i];
} catch (e) {
// will throw "Concurrent modification during iteration" error if modified at the same
// time [createTrade] is called, as this is not a normal map, but a sorted map
}
}
}
// Replace the old map with the new one
_sortedAvailableProviders.clear();
_sortedAvailableProviders.addAll(newSortedProviders);
if (_sortedAvailableProviders.isNotEmpty) bestRate = _sortedAvailableProviders.keys.first;
}
@@ -637,10 +649,26 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
// Ensure we have providers available before attempting to create trade
if (_sortedAvailableProviders.isEmpty) {
await calculateBestRate();
if (_sortedAvailableProviders.isEmpty) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange);
return;
}
}
try {
for (var i = 0; i < _sortedAvailableProviders.values.length; i++) {
final provider = _sortedAvailableProviders.values.toList()[i];
final providerRate = _sortedAvailableProviders.keys.toList()[i];
// snapshot of providers to avoid concurrent modification issues
final providersSnapshot = _sortedAvailableProviders.values.toList();
final ratesSnapshot = _sortedAvailableProviders.keys.toList();
for (var i = 0; i < providersSnapshot.length; i++) {
final provider = providersSnapshot[i];
final providerRate = ratesSnapshot[i];
if (!(await provider.checkIsAvailable())) continue;
@@ -675,8 +703,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
} else {
try {
if (provider is SwapTradeExchangeProvider) {
final destinationAmount = (fiatConversionStore.prices[request.toCurrency] ?? 0.00) * (double.tryParse(request.toAmount) ?? 0.00);
final sendingAmount = (fiatConversionStore.prices[request.fromCurrency] ?? 0.00) * (double.tryParse(request.fromAmount) ?? 0.00);
final destinationAmount = (fiatConversionStore.prices[request.toCurrency] ?? 0.00) *
(double.tryParse(request.toAmount) ?? 0.00);
final sendingAmount = (fiatConversionStore.prices[request.fromCurrency] ?? 0.00) *
(double.tryParse(request.fromAmount) ?? 0.00);
if (destinationAmount > 2000 || sendingAmount > 2000) {
continue;
@@ -1066,47 +1096,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
// Adding user's Erc20 tokens to the list of currencies
Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
final walletKey = walletInfo.name.replaceAll(" ", "_");
final boxName = switch (walletInfo.type) {
WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}',
WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}',
_ => '${walletKey}_${Erc20Token.ethereumBoxName}',
};
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<Erc20Token>(boxName);
}
return CakeHive.openBox<Erc20Token>(boxName);
}
Future<List<Erc20Token>> _loadAllUniqueEvmTokens() async {
final evmWallets = walletInfoSource.values.where(
(w) => w.type == WalletType.ethereum || w.type == WalletType.polygon,
);
final seen = <String>{};
final unique = <Erc20Token>[];
for (final wallet in evmWallets) {
final chain = wallet.type == WalletType.ethereum ? 'ETH' : 'POL';
final box = await _openEvmTokensBoxFor(wallet);
for (final t in box.values.where((t) => t.enabled)) {
final key = '$chain|${t.contractAddress.toLowerCase()}';
if (seen.add(key)) {
unique.add(t);
}
}
}
return unique;
}
@action
Future<void> _injectUserEthTokensIntoCurrencyLists() async {
final userTokens = await _loadAllUniqueEvmTokens();
final userTokens = await TokenUtilities.loadAllUniqueEvmTokens(walletInfoSource);
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
@@ -1132,35 +1124,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
// Adding user's Solana tokens to the list of currencies
Future<Box<SPLToken>> _openSolTokensBoxFor(WalletInfo wallet) async {
final boxName = '${wallet.name.replaceAll(" ", "_")}_${SPLToken.boxName}';
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<SPLToken>(boxName);
}
return CakeHive.openBox<SPLToken>(boxName);
}
Future<List<SPLToken>> _loadAllUniqueSolTokens() async {
final solWallets = walletInfoSource.values.where((wallet) => wallet.type == WalletType.solana);
final tokens = <SPLToken>[];
for (final wallet in solWallets) {
final box = await _openSolTokensBoxFor(wallet);
tokens.addAll(box.values.where((t) => t.enabled));
}
final seen = <String>{};
final unique = <SPLToken>[];
for (final token in tokens) {
final key = token.mintAddress.toLowerCase();
if (!seen.contains(key)) {
seen.add(key);
unique.add(token);
}
}
return unique;
}
bool _listContainsSplToken(List<CryptoCurrency> list, SPLToken token) {
return list.any((item) {
if (item is SPLToken) {
@@ -1173,7 +1136,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
Future<void> _injectUserSplTokensIntoCurrencyLists() async {
final userTokens = await _loadAllUniqueSolTokens();
final userTokens = await TokenUtilities.loadAllUniqueSolTokens(walletInfoSource);
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
@@ -1189,31 +1152,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
// Adding user's Tron tokens to the list of currencies
Future<Box<TronToken>> _openTronTokensBoxFor(WalletInfo walletInfo) async {
final boxName = '${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}';
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<TronToken>(boxName);
}
return CakeHive.openBox<TronToken>(boxName);
}
Future<List<TronToken>> _loadAllUniqueTronTokens() async {
final tronWallets = walletInfoSource.values.where((w) => w.type == WalletType.tron);
final seen = <String>{};
final unique = <TronToken>[];
for (final wallet in tronWallets) {
final box = await _openTronTokensBoxFor(wallet);
for (final t in box.values.where((t) => t.enabled)) {
final key = t.contractAddress.toLowerCase();
if (seen.add(key)) unique.add(t);
}
}
return unique;
}
bool _listContainsTronToken(List<CryptoCurrency> list, TronToken token) {
return list.any((item) {
if (item is TronToken) {
@@ -1226,7 +1164,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
Future<void> _injectUserTronTokensIntoCurrencyLists() async {
final userTokens = await _loadAllUniqueTronTokens();
final userTokens = await TokenUtilities.loadAllUniqueTronTokens(walletInfoSource);
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
+28 -8
View File
@@ -37,9 +37,7 @@ import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
@@ -53,10 +51,12 @@ import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/utils/token_utilities.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'send_view_model.g.dart';
@@ -72,14 +72,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
wallet.type == WalletType.solana ||
wallet.type == WalletType.tron ||
wallet.type == WalletType.zano;
for (final output in outputs) {
output.updateWallet(wallet);
}
// Update unspent coins list view model with the new wallet reference
unspentCoinsListViewModel.updateWallet(wallet);
// Update sending balance to reflect the new wallet's balance
updateSendingBalance();
}
@@ -95,7 +95,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
this.transactionDescriptionBox,
this.ledgerViewModel,
this.unspentCoinsListViewModel,
this.feesViewModel, {
this.feesViewModel,
this.walletInfoSource, {
this.coinTypeToSpendFrom = UnspentCoinType.nonMweb,
}) : state = InitialExecutionState(),
currencies = appStore.wallet!.balance.keys.toList(),
@@ -121,6 +122,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
ObservableList<Output> outputs;
final Box<WalletInfo> walletInfoSource;
@observable
UnspentCoinType coinTypeToSpendFrom;
@@ -337,8 +340,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
].contains(wallet.type);
@computed
bool get isElectrumWallet =>
[WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, WalletType.dogecoin].contains(wallet.type);
bool get isElectrumWallet => [
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.dogecoin
].contains(wallet.type);
@observable
CryptoCurrency selectedCryptoCurrency;
@@ -924,4 +931,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@observable
String? payjoinUri;
@action
Future<void> fetchTokenForContractAddress(String contractAddress) async {
final token = await TokenUtilities.findTokenByAddress(
walletType: wallet.type,
walletInfoSource: walletInfoSource,
address: contractAddress,
);
if (token != null) {
selectedCryptoCurrency = token as CryptoCurrency;
}
}
}
@@ -3,6 +3,7 @@ import 'dart:core';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/core/payment_uris.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
@@ -37,228 +38,6 @@ part 'wallet_address_list_view_model.g.dart';
class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel;
abstract class PaymentURI {
PaymentURI({required this.amount, required this.address});
final String amount;
final String address;
}
class MoneroURI extends PaymentURI {
MoneroURI({required super.amount, required super.address});
@override
String toString() {
var base = 'monero:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class HavenURI extends PaymentURI {
HavenURI({required super.amount, required super.address});
@override
String toString() {
var base = 'haven:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BitcoinURI extends PaymentURI {
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
final String pjUri;
@override
String toString() {
final qp = <String, String>{};
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
qp['pjos'] = '0';
qp['pj'] = pjUri;
}
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
}
}
class LitecoinURI extends PaymentURI {
LitecoinURI({required super.amount, required super.address});
@override
String toString() {
var base = 'litecoin:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class EthereumURI extends PaymentURI {
EthereumURI({required super.amount, required super.address});
@override
String toString() {
var base = 'ethereum:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BitcoinCashURI extends PaymentURI {
BitcoinCashURI({required super.amount, required super.address});
@override
String toString() {
var base = address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class NanoURI extends PaymentURI {
NanoURI({required super.amount, required super.address});
@override
String toString() {
var base = 'nano:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class PolygonURI extends PaymentURI {
PolygonURI({required super.amount, required super.address});
@override
String toString() {
var base = 'polygon:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class SolanaURI extends PaymentURI {
SolanaURI({required super.amount, required super.address});
@override
String toString() {
var base = 'solana:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class TronURI extends PaymentURI {
TronURI({required super.amount, required super.address});
@override
String toString() {
var base = 'tron:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class WowneroURI extends PaymentURI {
WowneroURI({required super.amount, required super.address});
@override
String toString() {
var base = 'wownero:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class ZanoURI extends PaymentURI {
ZanoURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'zano:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class DecredURI extends PaymentURI {
DecredURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'decred:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class DogeURI extends PaymentURI {
DogeURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'doge:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store {
WalletAddressListViewModelBase({