mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'cake-tech:dev' into dev
This commit is contained in:
@@ -17,6 +17,26 @@ extension MaxDecimals on String {
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
String withDecimals(int decimals) {
|
||||
var parts = split(".");
|
||||
|
||||
if (parts.length > 2) {
|
||||
parts = [parts.first, parts.sublist(1).join("")];
|
||||
}
|
||||
|
||||
if (parts.length == 1) {
|
||||
parts.add("");
|
||||
}
|
||||
|
||||
if (parts[1].length > decimals) {
|
||||
parts[1] = parts[1].substring(0, decimals);
|
||||
} else {
|
||||
parts[1] = parts[1].padRight(decimals, '0');
|
||||
}
|
||||
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
|
||||
/// Format a stringified number to a localized representation
|
||||
/// 1.000.000,00 in de_DE
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
|
||||
import 'package:cw_core/crypto_amount_format.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
||||
class AmountParsingProxy {
|
||||
final BitcoinAmountDisplayMode displayMode;
|
||||
@@ -19,11 +20,19 @@ class AmountParsingProxy {
|
||||
|
||||
/// [getCryptoOutputAmount] turns the input [amount] into the preferred representation of [cryptoCurrency]
|
||||
String getDisplayCryptoAmount(String amount, CryptoCurrency cryptoCurrency) {
|
||||
if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) {
|
||||
return cryptoCurrency.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)).toString();
|
||||
|
||||
try {
|
||||
if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) {
|
||||
return cryptoCurrency.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)).toString();
|
||||
}
|
||||
|
||||
return amount.withMaxDecimals(cryptoCurrency.decimals);
|
||||
|
||||
} catch(_) {
|
||||
printV("failed to parse amount $amount for currency ${cryptoCurrency.title}, falling back to showing unparsed");
|
||||
return amount;
|
||||
}
|
||||
|
||||
return amount.withMaxDecimals(cryptoCurrency.decimals);
|
||||
}
|
||||
|
||||
/// [getCryptoStringRepresentation] turns the input [amount] into the preferred representation of [cryptoCurrency]
|
||||
|
||||
@@ -127,7 +127,6 @@ class TradeMonitor {
|
||||
|
||||
bool _shouldSkipTrade(Trade trade, String walletId, ExchangeProvider? provider) {
|
||||
if (trade.walletId != walletId) {
|
||||
printV('Skipping trade ${trade.id} because it\'s not for this wallet');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ String calculateFiatAmount({double? price, String? cryptoAmount, bool raw = fals
|
||||
|
||||
cryptoAmount = cryptoAmount.replaceAll(',', '.');
|
||||
|
||||
final _amount = double.parse(cryptoAmount);
|
||||
final _amount = double.tryParse(cryptoAmount);
|
||||
if (_amount == null || _amount.isNaN) return '0.00';
|
||||
final _result = price * _amount;
|
||||
final result = _result < 0 ? _result * -1 : _result;
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class ChainflipExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
|
||||
@@ -23,7 +23,6 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
: _settingsStore = settingsStore,
|
||||
_lastUsedRateId = '';
|
||||
|
||||
|
||||
static final apiKey =
|
||||
isMoneroOnly ? secrets.changeNowMoneroApiKey : secrets.changeNowCakeWalletApiKey;
|
||||
static const apiAuthority = 'api.changenow.io';
|
||||
@@ -55,7 +54,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
@@ -81,18 +80,19 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
return Limits(
|
||||
min: responseJSON['minAmount'] as double?, max: responseJSON['maxAmount'] as double?);
|
||||
final min = double.tryParse(responseJSON['minAmount']?.toString() ?? '');
|
||||
final max = double.tryParse(responseJSON['maxAmount']?.toString() ?? '');
|
||||
if (max == 0) return null;
|
||||
return Limits(min: min, max: max);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> fetchRate({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode,
|
||||
required bool isReceiveAmount
|
||||
}) async {
|
||||
Future<double> fetchRate(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode,
|
||||
required bool isReceiveAmount}) async {
|
||||
try {
|
||||
if (amount == 0) return 0.0;
|
||||
|
||||
@@ -117,8 +117,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final fromAmount = double.parse(responseJSON['fromAmount'].toString());
|
||||
final toAmount = double.parse(responseJSON['toAmount'].toString());
|
||||
final fromAmount = double.tryParse(responseJSON['fromAmount']?.toString() ?? '') ?? 0.0;
|
||||
final toAmount = double.tryParse(responseJSON['toAmount']?.toString() ?? '') ?? 0.0;
|
||||
if (fromAmount <= 0 || toAmount <= 0) return 0.0;
|
||||
final rateId = responseJSON['rateId'] as String? ?? '';
|
||||
|
||||
if (rateId.isNotEmpty) _lastUsedRateId = rateId;
|
||||
@@ -278,14 +279,18 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
final fromCurrency = responseJSON['fromCurrency'] as String;
|
||||
final fromNetwork = responseJSON['fromNetwork'] as String?;
|
||||
final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
|
||||
final fromTag = fromCurrency.toUpperCase() == _normalizedFromNetwork.toUpperCase() ? null : _normalizedFromNetwork;
|
||||
final fromTag = fromCurrency.toUpperCase() == _normalizedFromNetwork.toUpperCase()
|
||||
? null
|
||||
: _normalizedFromNetwork;
|
||||
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
|
||||
|
||||
// Parsing 'to' currency
|
||||
final toCurrency = responseJSON['toCurrency'] as String;
|
||||
final toNetwork = responseJSON['toNetwork'] as String?;
|
||||
final _normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
|
||||
final toTag = toCurrency.toUpperCase() == _normalizedToNetwork.toUpperCase() ? null : _normalizedToNetwork;
|
||||
final toTag = toCurrency.toUpperCase() == _normalizedToNetwork.toUpperCase()
|
||||
? null
|
||||
: _normalizedToNetwork;
|
||||
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
|
||||
|
||||
final inputAddress = responseJSON['payinAddress'] as String;
|
||||
@@ -310,7 +315,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
expiredAt: expiredAt,
|
||||
outputTransaction: outputTransaction,
|
||||
payoutAddress: payoutAddress,
|
||||
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
|
||||
userCurrencyFromRaw:
|
||||
'${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
|
||||
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}',
|
||||
);
|
||||
}
|
||||
@@ -363,6 +369,4 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
_ => network,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ abstract class ExchangeProvider {
|
||||
@override
|
||||
String toString() => title;
|
||||
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode});
|
||||
|
||||
Future<Trade> createTrade(
|
||||
|
||||
@@ -42,7 +42,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits({
|
||||
Future<Limits?> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
@@ -143,7 +143,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
|
||||
throw Exception(message);
|
||||
}
|
||||
|
||||
final rate = responseJSON['rate'] as double;
|
||||
final rate = double.tryParse(responseJSON['rate']?.toString() ?? '') ?? 0.0;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
|
||||
@@ -72,7 +72,7 @@ class JupiterExchangeProvider extends ExchangeProvider {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits({
|
||||
Future<Limits?> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
|
||||
@@ -47,7 +47,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
|
||||
@@ -97,7 +97,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
|
||||
@@ -60,7 +60,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
|
||||
@@ -51,7 +51,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
|
||||
@@ -43,7 +43,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
|
||||
@@ -62,7 +62,7 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits({
|
||||
Future<Limits?> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:cake_wallet/exchange/trade_not_found_exception.dart';
|
||||
import 'package:cake_wallet/exchange/trade_request.dart';
|
||||
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:cw_core/utils/print_verbose.dart';
|
||||
@@ -47,7 +46,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits({
|
||||
Future<Limits?> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
@@ -55,29 +54,33 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
|
||||
try {
|
||||
final uri = Uri.https(apiAuthority, getCoins);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode != 200)
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
|
||||
final coinsInfo = responseJSON['data'] as List<dynamic>;
|
||||
final coinsInfoRaw = responseJSON['data'];
|
||||
final coinsInfo = coinsInfoRaw is List<dynamic> ? coinsInfoRaw : <dynamic>[];
|
||||
|
||||
final coin = coinsInfo.firstWhere(
|
||||
(coin) => coin['id'].toString().toUpperCase() == _normalizeCurrency(from),
|
||||
orElse: () => null,
|
||||
final normalized = _normalizeCurrency(from);
|
||||
final coin = coinsInfo.cast<Map<String, dynamic>>().firstWhere(
|
||||
(c) => (c['id']?.toString().toUpperCase() ?? '') == normalized,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
|
||||
if (coin == null) throw Exception('Coin not found: ${_normalizeCurrency(from)}');
|
||||
if (coin.isEmpty) {
|
||||
// Currency not supported by SwapTrade (e.g. USDC, DOGE).
|
||||
return null;
|
||||
}
|
||||
|
||||
return Limits(
|
||||
min: double.parse(coin['min'].toString()),
|
||||
max: double.parse(coin['max'].toString()),
|
||||
);
|
||||
final min = double.tryParse(coin['min']?.toString() ?? '') ?? 0.0;
|
||||
final max = double.tryParse(coin['max']?.toString() ?? '') ?? 0.0;
|
||||
if (max == 0) return null;
|
||||
return Limits(min: min, max: max);
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
throw Exception('Error fetching limits: ${e.toString()}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +192,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
|
||||
'amount_send': request.fromAmount,
|
||||
'recipient': request.toAddress,
|
||||
'ref': 'cake',
|
||||
'markup': markup,
|
||||
'markup': int.tryParse(markup.toString()) ?? 0,
|
||||
'refund_address': request.refundAddress,
|
||||
};
|
||||
|
||||
@@ -394,7 +397,15 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
|
||||
CryptoCurrency.bnb => 'BNB_BSC',
|
||||
CryptoCurrency.usdterc20 => 'USDT_ERC20',
|
||||
CryptoCurrency.usdttrc20 => 'TRX_USDT_S2UZ',
|
||||
_ => '',
|
||||
CryptoCurrency.usdtbsc => 'USDT_BSC',
|
||||
CryptoCurrency.sol => 'SOL',
|
||||
CryptoCurrency.btc => 'BTC',
|
||||
CryptoCurrency.xmr => 'XMR',
|
||||
CryptoCurrency.ltc => 'LTC',
|
||||
CryptoCurrency.ada => 'ADA',
|
||||
CryptoCurrency.bch => 'BCH',
|
||||
CryptoCurrency.zec => 'ZEC',
|
||||
_ => currency.title.toUpperCase(),
|
||||
};
|
||||
return network;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ class ThorChainExchangeProvider extends ExchangeProvider {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
|
||||
@@ -82,7 +82,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits> fetchLimits(
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
@@ -103,10 +103,10 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
|
||||
final coinJson = responseJSON.first as Map<String, dynamic>;
|
||||
|
||||
return Limits(
|
||||
min: coinJson['minimum'] as double?,
|
||||
max: coinJson['maximum'] as double?,
|
||||
);
|
||||
final min = double.tryParse(coinJson['minimum']?.toString() ?? '');
|
||||
final max = double.tryParse(coinJson['maximum']?.toString() ?? '');
|
||||
if (max == 0) return null;
|
||||
return Limits(min: min, max: max);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -135,11 +135,18 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey});
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final fromAmount = double.parse(responseJSON['amount_from'].toString());
|
||||
final toAmount = double.parse(responseJSON['amount_to'].toString());
|
||||
final rateId = responseJSON['trade_id'] as String? ?? '';
|
||||
final fromAmount = double.tryParse(responseJSON['amount_from']?.toString() ?? '') ?? 0.0;
|
||||
final toAmount = double.tryParse(responseJSON['amount_to']?.toString() ?? '') ?? 0.0;
|
||||
|
||||
var quotes = responseJSON['quotes']['quotes'] as List;
|
||||
if (fromAmount <= 0 || toAmount <= 0) return 0.0;
|
||||
|
||||
final quotesData = responseJSON['quotes'];
|
||||
final rateId = _safeString(responseJSON, 'trade_id');
|
||||
|
||||
if (quotesData == null || quotesData is! Map<String, dynamic>) {
|
||||
return 0.0;
|
||||
}
|
||||
final quotes = (quotesData['quotes'] as List?) ?? [];
|
||||
_provider = quotes
|
||||
.where((quote) => providerStates[quote['provider']] != false)
|
||||
.map((quote) => quote['provider'])
|
||||
@@ -243,7 +250,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
params['id'] = _lastUsedRateId;
|
||||
}
|
||||
|
||||
if (_provider.isEmpty) {
|
||||
if (_provider.isEmpty || _provider.first == null || _provider.first.toString().isEmpty) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
@@ -264,15 +271,15 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
throw Exception('No available provider is enabled');
|
||||
}
|
||||
|
||||
params['provider'] = _provider.first as String;
|
||||
params['provider'] = _provider.first.toString();
|
||||
|
||||
final uri = await _getUri(createTradePath, params);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey});
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['error'] as String;
|
||||
final message = responseJSON['message'] as String;
|
||||
final error = _safeString(responseJSON, 'error', 'Unknown error');
|
||||
final message = _safeString(responseJSON, 'message');
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
@@ -319,18 +326,18 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final id = responseJSON['trade_id'] as String;
|
||||
final inputAddress = responseJSON['address_provider'] as String;
|
||||
final refundAddress = responseJSON['refund_address'] as String;
|
||||
final status = responseJSON['status'] as String;
|
||||
final payoutAddress = responseJSON['address_user'] as String;
|
||||
final date = responseJSON['date'] as String;
|
||||
final password = responseJSON['password'] as String;
|
||||
final providerId = responseJSON['id_provider'] as String;
|
||||
final providerName = responseJSON['provider'] as String;
|
||||
final amount = responseJSON['amount_from']?.toString();
|
||||
final receiveAmount = responseJSON['amount_to']?.toString();
|
||||
final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
|
||||
final id = _safeString(responseJSON, 'trade_id');
|
||||
final inputAddress = _safeString(responseJSON, 'address_provider');
|
||||
final refundAddress = _safeString(responseJSON, 'refund_address');
|
||||
final status = _safeString(responseJSON, 'status');
|
||||
final payoutAddress = _safeString(responseJSON, 'address_user');
|
||||
final date = _safeString(responseJSON, 'date');
|
||||
final password = _safeString(responseJSON, 'password');
|
||||
final providerId = _safeString(responseJSON, 'id_provider');
|
||||
final providerName = _safeString(responseJSON, 'provider');
|
||||
final amount = _safeString(responseJSON, 'amount_from');
|
||||
final receiveAmount = _safeString(responseJSON, 'amount_to');
|
||||
final addressProviderMemo = _safeString(responseJSON, 'address_provider_memo');
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
@@ -365,6 +372,9 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
},
|
||||
);
|
||||
|
||||
final amountToTrade = amount.isEmpty ? request.fromAmount : amount;
|
||||
final receiveAmountToTrade = receiveAmount.isEmpty ? request.toAmount : receiveAmount;
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: request.fromCurrency,
|
||||
@@ -377,8 +387,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
providerId: providerId,
|
||||
providerName: providerName,
|
||||
createdAt: DateTime.tryParse(date)?.toLocal(),
|
||||
amount: amount ?? request.fromAmount,
|
||||
receiveAmount: receiveAmount ?? request.toAmount,
|
||||
amount: amountToTrade,
|
||||
receiveAmount: receiveAmountToTrade,
|
||||
payoutAddress: payoutAddress,
|
||||
isSendAll: isSendAll,
|
||||
extraId: addressProviderMemo,
|
||||
@@ -396,19 +406,21 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
|
||||
final responseListJson = json.decode(response.body) as List;
|
||||
final responseJSON = responseListJson.first;
|
||||
final id = responseJSON['trade_id'] as String;
|
||||
final payoutAddress = responseJSON['address_user'] as String;
|
||||
final refundAddress = responseJSON['refund_address'] as String;
|
||||
final inputAddress = responseJSON['address_provider'] as String;
|
||||
final responseJSON = responseListJson.first as Map<String, dynamic>;
|
||||
final id = _safeString(responseJSON, 'trade_id');
|
||||
final payoutAddress = _safeString(responseJSON, 'address_user');
|
||||
final refundAddress = _safeString(responseJSON, 'refund_address');
|
||||
final inputAddress = _safeString(responseJSON, 'address_provider');
|
||||
final fromAmount = responseJSON['amount_from']?.toString() ?? '0';
|
||||
final password = responseJSON['password'] as String;
|
||||
final providerId = responseJSON['id_provider'] as String;
|
||||
final providerName = responseJSON['provider'] as String;
|
||||
final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
|
||||
final password = _safeString(responseJSON, 'password');
|
||||
final providerId = _safeString(responseJSON, 'id_provider');
|
||||
final providerName = _safeString(responseJSON, 'provider');
|
||||
final memoVal = _safeString(responseJSON, 'address_provider_memo');
|
||||
final addressProviderMemo = memoVal.isEmpty ? null : memoVal;
|
||||
|
||||
final fromCurrency = responseJSON['ticker_from'] as String;
|
||||
final fromNetwork = responseJSON['network_from'] as String?;
|
||||
final fromCurrency = _safeString(responseJSON, 'ticker_from');
|
||||
final fromNetworkVal = _safeString(responseJSON, 'network_from');
|
||||
final fromNetwork = fromNetworkVal.isEmpty ? null : fromNetworkVal;
|
||||
final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
|
||||
final fromTag = _normalizedFromNetwork.isEmpty ||
|
||||
_normalizedFromNetwork == fromCurrency.toUpperCase() ||
|
||||
@@ -418,8 +430,9 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
|
||||
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
|
||||
|
||||
final toCurrency = responseJSON['ticker_to'] as String;
|
||||
final networkTo = responseJSON['network_to'] as String?;
|
||||
final toCurrency = _safeString(responseJSON, 'ticker_to');
|
||||
final networkToVal = _safeString(responseJSON, 'network_to');
|
||||
final networkTo = networkToVal.isEmpty ? null : networkToVal;
|
||||
final _normalizedToNetwork = _normalizeNetworkType(networkTo ?? '');
|
||||
final toTag = _normalizedToNetwork.isEmpty ||
|
||||
_normalizedToNetwork == toCurrency.toUpperCase() ||
|
||||
@@ -435,9 +448,9 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
refundAddress: refundAddress,
|
||||
createdAt: DateTime.parse(responseJSON['date'] as String),
|
||||
createdAt: DateTime.tryParse(_safeString(responseJSON, 'date')),
|
||||
amount: fromAmount,
|
||||
state: TradeState.deserialize(raw: responseJSON['status'] as String),
|
||||
state: TradeState.deserialize(raw: _safeString(responseJSON, 'status')),
|
||||
payoutAddress: payoutAddress,
|
||||
password: password,
|
||||
providerId: providerId,
|
||||
@@ -541,6 +554,14 @@ class TrocadorExchangeProvider extends ExchangeProvider {
|
||||
return Uri.https(clearNetAuthority, path, queryParams);
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe string extraction from API response. Handles different data types.
|
||||
static String _safeString(Map<String, dynamic> m, String key, [String nullError = '']) {
|
||||
final v = m[key];
|
||||
if (v == null) return nullError;
|
||||
if (v is String) return v;
|
||||
return v.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class TrocadorPartners {
|
||||
|
||||
@@ -150,7 +150,7 @@ class XOSwapExchangeProvider extends ExchangeProvider {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Limits> fetchLimits({
|
||||
Future<Limits?> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dar
|
||||
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
|
||||
import 'package:cw_core/balance_card_style_settings.dart';
|
||||
import 'package:cw_core/card_design.dart';
|
||||
import 'package:cw_core/generate_name.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
@@ -381,7 +382,9 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
|
||||
selected: true,
|
||||
designSwitchDuration: Duration(milliseconds: 200),
|
||||
width: cardWidth,
|
||||
design: widget.dashboardViewModel.cardDesigns[i],
|
||||
design: i >= widget.dashboardViewModel.cardDesigns.length
|
||||
? CardDesign.genericDefault
|
||||
: widget.dashboardViewModel.cardDesigns[i],
|
||||
),
|
||||
order: i,
|
||||
accountListItem: accounts[i]));
|
||||
|
||||
@@ -292,7 +292,10 @@ class _NewSwapPageState extends State<NewSwapPage> {
|
||||
|
||||
_depositAmountDebounce.run(() {
|
||||
widget.exchangeViewModel.calculateBestRate();
|
||||
widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
|
||||
if (depositAmountController.text != widget.exchangeViewModel.depositAmount &&
|
||||
depositAmountController.text != S.of(context).all) {
|
||||
widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
|
||||
}
|
||||
widget.exchangeViewModel.isReceiveAmountEntered = false;
|
||||
widget.exchangeViewModel.isFixedRateMode = false;
|
||||
if (!receiveKey.currentState!.amountFocusNode.hasFocus) {
|
||||
|
||||
@@ -57,7 +57,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
|
||||
if (widget.showOtherWallets) {
|
||||
() async {
|
||||
items.addAll((await WalletInfo.getAll())
|
||||
.where((item) => item.type == widget.sendViewModel.walletType));
|
||||
.where((item) => item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null));
|
||||
items.sort((a, b) {
|
||||
if (a.name == widget.sendViewModel.wallet.name)
|
||||
return -1;
|
||||
|
||||
@@ -154,8 +154,10 @@ class SendTransactionDetails extends StatelessWidget {
|
||||
String sumStr<T>(List<T> list, double Function(T) picker) =>
|
||||
sumBy(list, picker).toString();
|
||||
|
||||
String sumWithUnit<T>(List<T> list, double Function(T) picker, String unit) =>
|
||||
"${sumStr(list, picker)} $unit";
|
||||
String sumWithUnit<T>(List<T> list, double Function(T) picker, String unit, {int? decimals}) {
|
||||
final str = sumStr(list, picker);
|
||||
return "${decimals == null ? str : str.withDecimals(decimals)} $unit";
|
||||
}
|
||||
|
||||
|
||||
Widget _buildMainContent(BuildContext context) {
|
||||
@@ -198,6 +200,7 @@ class SendTransactionDetails extends StatelessWidget {
|
||||
sendViewModel.outputs,
|
||||
(o) => double.tryParse(o.fiatAmount.replaceAll(",", "")) ?? 0,
|
||||
sendViewModel.fiatCurrency.title,
|
||||
decimals:2
|
||||
)
|
||||
: sendViewModel.pendingTransactionFiatAmountFormatted;
|
||||
|
||||
@@ -206,6 +209,7 @@ class SendTransactionDetails extends StatelessWidget {
|
||||
sendViewModel.outputs,
|
||||
(o) => double.tryParse(o.estimatedFeeFiatAmount.replaceAll(",", "")) ?? 0,
|
||||
sendViewModel.fiatCurrency.title,
|
||||
decimals:2
|
||||
)
|
||||
: sendViewModel.pendingTransactionFeeFiatAmountFormatted;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class SwapLimitPopup extends StatelessWidget {
|
||||
final double? amount = double.tryParse(exchangeViewModel.depositAmountCanonical);
|
||||
final max = exchangeViewModel.limits.max ?? double.infinity;
|
||||
final min = exchangeViewModel.limits.min ?? 0;
|
||||
final tooLarge = amount != null && amount > max;
|
||||
final tooLarge = amount != null && max != 0 && amount > max;
|
||||
final tooSmall = amount != null && min != 0 && amount < min;
|
||||
final show = amount != null && (tooLarge || tooSmall);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:cake_wallet/entities/new_main_actions.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
@@ -123,124 +124,126 @@ class _NEWNewMainNavBarState extends State<NewMainNavBar> {
|
||||
final activeColor = theme.colorScheme.onSurface;
|
||||
final inactiveColor = theme.colorScheme.primary;
|
||||
|
||||
final visibleActions = NewMainActions.all
|
||||
.where(
|
||||
(action) => action.canShow?.call(widget.dashboardViewModel) ?? true)
|
||||
.toList();
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
final visibleActions = NewMainActions.all
|
||||
.where((action) => action.canShow?.call(widget.dashboardViewModel) ?? true)
|
||||
.toList();
|
||||
|
||||
final pillWidth = _estimatePillWidthForAction(
|
||||
context, visibleActions[widget.selectedIndex],
|
||||
color: activeColor);
|
||||
final pillWidth = _estimatePillWidthForAction(context, visibleActions[widget.selectedIndex],
|
||||
color: activeColor);
|
||||
|
||||
final barWidth = calcBarWidth(pillWidth, visibleActions.length);
|
||||
final barWidth = calcBarWidth(pillWidth, visibleActions.length);
|
||||
|
||||
final currentAction = visibleActions[widget.selectedIndex];
|
||||
final currentAction = visibleActions[widget.selectedIndex];
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SafeArea(
|
||||
bottom: !(Platform.isIOS),
|
||||
top: false,
|
||||
child: Padding(
|
||||
// tux PLEASE consult me (malik) before removing this padding.
|
||||
padding: EdgeInsets.only(bottom: NewMainNavBar.barBottomPadding),
|
||||
child: AnimatedContainer(
|
||||
duration: barResizeDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width: barWidth,
|
||||
child: ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(barBorderRadius),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
|
||||
child: Container(
|
||||
height: NewMainNavBar.barHeight,
|
||||
decoration: ShapeDecoration(
|
||||
color: backgroundColor,
|
||||
shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(barBorderRadius),
|
||||
side: const BorderSide(color: Color(0x14FFFFFF), width: 1),
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SafeArea(
|
||||
bottom: !(Platform.isIOS),
|
||||
top: false,
|
||||
child: Padding(
|
||||
// tux PLEASE consult me (malik) before removing this padding.
|
||||
padding: EdgeInsets.only(bottom: NewMainNavBar.barBottomPadding),
|
||||
child: AnimatedContainer(
|
||||
duration: barResizeDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width: barWidth,
|
||||
child: ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(barBorderRadius),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
|
||||
child: Container(
|
||||
height: NewMainNavBar.barHeight,
|
||||
decoration: ShapeDecoration(
|
||||
color: backgroundColor,
|
||||
shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(barBorderRadius),
|
||||
side: const BorderSide(color: Color(0x14FFFFFF), width: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedPill(
|
||||
left: calcLeft(widget.selectedIndex, pillWidth),
|
||||
pillColor: pillColor,
|
||||
currentAction: currentAction,
|
||||
pillIconHeight: pillIconHeight,
|
||||
pillIconWidth: pillIconWidth,
|
||||
pillIconSpacing: pillIconSpacing,
|
||||
pillBorderRadius: pillBorderRadius,
|
||||
contentColor: activeColor,
|
||||
estimateWidthForAction: pillWidth,
|
||||
pillTextStyle: pillTextStyle,
|
||||
pillMoveDuration: pillMoveDuration,
|
||||
pillResizeDuration: pillResizeDuration,
|
||||
),
|
||||
for (int i = 0; i < visibleActions.length; i++)
|
||||
AnimatedPositioned(
|
||||
duration: pillResizeDuration,
|
||||
width: iconBoxWidth,
|
||||
left: calcLeft(i, pillWidth)+((i == widget.selectedIndex) ? iconHorizontalPadding/100 : 0),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: InkWell(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
splashColor: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(pillBorderRadius),
|
||||
onTap: () => _onItemTap(i),
|
||||
child: AnimatedContainer(
|
||||
duration: _firstFrame
|
||||
? Duration.zero
|
||||
: inactiveIconMoveDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width:
|
||||
i == widget.selectedIndex ? pillWidth : iconBoxWidth,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedAlign(
|
||||
duration: inactiveIconFadeDuration,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedPill(
|
||||
left: calcLeft(widget.selectedIndex, pillWidth),
|
||||
pillColor: pillColor,
|
||||
currentAction: currentAction,
|
||||
pillIconHeight: pillIconHeight,
|
||||
pillIconWidth: pillIconWidth,
|
||||
pillIconSpacing: pillIconSpacing,
|
||||
pillBorderRadius: pillBorderRadius,
|
||||
contentColor: activeColor,
|
||||
estimateWidthForAction: pillWidth,
|
||||
pillTextStyle: pillTextStyle,
|
||||
pillMoveDuration: pillMoveDuration,
|
||||
pillResizeDuration: pillResizeDuration,
|
||||
),
|
||||
for (int i = 0; i < visibleActions.length; i++)
|
||||
AnimatedPositioned(
|
||||
duration: pillResizeDuration,
|
||||
width: iconBoxWidth,
|
||||
left: calcLeft(i, pillWidth)+((i == widget.selectedIndex) ? iconHorizontalPadding/100 : 0),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: InkWell(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
splashColor: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(pillBorderRadius),
|
||||
onTap: () => _onItemTap(i),
|
||||
child: AnimatedContainer(
|
||||
duration: _firstFrame
|
||||
? Duration.zero
|
||||
: inactiveIconMoveDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width:
|
||||
i == widget.selectedIndex ? pillWidth : iconBoxWidth,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedScale(
|
||||
duration: inactiveIconAppearDuration,
|
||||
child: AnimatedAlign(
|
||||
duration: inactiveIconFadeDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
|
||||
child: TweenAnimationBuilder<Color?>(
|
||||
tween: ColorTween(
|
||||
begin: (i == widget.selectedIndex) ? inactiveColor : activeColor,
|
||||
end: (i==widget.selectedIndex) ? activeColor : inactiveColor,
|
||||
),
|
||||
duration: iconColorChangeDuration,
|
||||
builder: (context, value, child) {
|
||||
return Container(
|
||||
height: NewMainNavBar.barHeight,
|
||||
child: CakeImageWidget(imageUrl:
|
||||
visibleActions[i].image,
|
||||
width: iconWidth,
|
||||
height: iconHeight,
|
||||
//fit: BoxFit.scaleDown,
|
||||
colorFilter: ColorFilter.mode(
|
||||
value ?? inactiveColor,
|
||||
BlendMode.srcIn,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedScale(
|
||||
duration: inactiveIconAppearDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
|
||||
child: TweenAnimationBuilder<Color?>(
|
||||
tween: ColorTween(
|
||||
begin: (i == widget.selectedIndex) ? inactiveColor : activeColor,
|
||||
end: (i==widget.selectedIndex) ? activeColor : inactiveColor,
|
||||
),
|
||||
duration: iconColorChangeDuration,
|
||||
builder: (context, value, child) {
|
||||
return Container(
|
||||
height: NewMainNavBar.barHeight,
|
||||
child: CakeImageWidget(imageUrl:
|
||||
visibleActions[i].image,
|
||||
width: iconWidth,
|
||||
height: iconHeight,
|
||||
//fit: BoxFit.scaleDown,
|
||||
colorFilter: ColorFilter.mode(
|
||||
value ?? inactiveColor,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ class _StatsCardState extends State<_StatsCard> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(entry.key.title),
|
||||
Text(entry.key?.title ?? 'Unknown'),
|
||||
Text(entry.value.toString()),
|
||||
],
|
||||
),
|
||||
@@ -310,7 +310,7 @@ class _LogEntryCard extends StatelessWidget {
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${log.provider.title} - ${log.function}",
|
||||
"${log.provider?.title ?? 'Unknown/Empty'} - ${log.function}",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'package:cake_wallet/core/utilities.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
||||
|
||||
class ExchangeProviderLogEntry {
|
||||
final DateTime timestamp;
|
||||
final ExchangeProviderDescription provider;
|
||||
final ExchangeProviderDescription? provider;
|
||||
final String function;
|
||||
final String? error;
|
||||
final String? stackTrace;
|
||||
@@ -14,8 +15,8 @@ class ExchangeProviderLogEntry {
|
||||
|
||||
ExchangeProviderLogEntry({
|
||||
required this.timestamp,
|
||||
required this.provider,
|
||||
required this.function,
|
||||
this.provider,
|
||||
this.error,
|
||||
this.stackTrace,
|
||||
this.callStack,
|
||||
@@ -26,7 +27,7 @@ class ExchangeProviderLogEntry {
|
||||
|
||||
String toLogString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('Provider: ${provider.title}');
|
||||
buffer.writeln('Provider: ${provider?.title ?? 'Unknown/Empty'}');
|
||||
buffer.writeln('Function: $function');
|
||||
buffer.writeln('Timestamp: ${timestamp.toIso8601String()}');
|
||||
buffer.writeln('Success: $isSuccess');
|
||||
@@ -58,7 +59,7 @@ class ExchangeProviderLogEntry {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'provider': provider.title,
|
||||
'provider': provider?.title,
|
||||
'function': function,
|
||||
'error': error,
|
||||
'stackTrace': stackTrace,
|
||||
@@ -85,14 +86,14 @@ class ExchangeProviderLogEntry {
|
||||
ExchangeProviderDescription.stealthEx,
|
||||
ExchangeProviderDescription.chainflip,
|
||||
ExchangeProviderDescription.xoSwap,
|
||||
ExchangeProviderDescription.swapsXyz,
|
||||
ExchangeProviderDescription.nearIntents,
|
||||
ExchangeProviderDescription.jupiter,
|
||||
];
|
||||
|
||||
return ExchangeProviderLogEntry(
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
provider: allProviders.firstWhere(
|
||||
(p) => p.title == json['provider'] as String,
|
||||
orElse: () => ExchangeProviderDescription.changeNow,
|
||||
),
|
||||
provider: allProviders.firstWhereOrNull((p) => p.title == json['provider'] as String),
|
||||
function: json['function'] as String,
|
||||
error: json['error'] as String?,
|
||||
stackTrace: json['stackTrace'] as String?,
|
||||
@@ -131,9 +132,9 @@ class ExchangeProviderLogger {
|
||||
}
|
||||
|
||||
static void logError({
|
||||
required ExchangeProviderDescription provider,
|
||||
required String function,
|
||||
required dynamic error,
|
||||
ExchangeProviderDescription? provider,
|
||||
StackTrace? stackTrace,
|
||||
Map<String, dynamic>? requestData,
|
||||
String? callStack,
|
||||
|
||||
@@ -43,10 +43,11 @@ abstract class ExchangeProviderLogsViewModelBase with Store {
|
||||
int get errorLogs => logs.where((log) => !log.isSuccess).length;
|
||||
|
||||
@computed
|
||||
Map<ExchangeProviderDescription, int> get logsByProvider {
|
||||
Map<ExchangeProviderDescription?, int> get logsByProvider {
|
||||
final Map<ExchangeProviderDescription, int> counts = {};
|
||||
for (final log in logs) {
|
||||
counts[log.provider] = (counts[log.provider] ?? 0) + 1;
|
||||
if (log.provider == null) continue;
|
||||
counts[log.provider!] = (counts[log.provider!] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import 'package:cake_wallet/exchange/trade_request.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
@@ -321,7 +322,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
|
||||
final List<ExchangeProvider> _tradeAvailableProviders = [];
|
||||
|
||||
Map<ExchangeProvider, Limits> _providerLimits = {};
|
||||
Map<ExchangeProvider, Limits?> _providerLimits = {};
|
||||
|
||||
@observable
|
||||
ObservableList<ExchangeProvider> selectedProviders;
|
||||
@@ -681,14 +682,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
if (receiveAmountFiat.split(".").last.length <= digits) {
|
||||
return receiveAmountFiat;
|
||||
}
|
||||
return double.parse(receiveAmountFiat).toStringAsPrecision(digits);
|
||||
|
||||
return double.tryParse(receiveAmountFiat)?.toStringAsPrecision(digits) ?? '0.00';
|
||||
}
|
||||
|
||||
String roundedDepositAmountFiat(int digits) {
|
||||
if (depositAmountFiat.split(".").last.length <= digits) {
|
||||
return depositAmountFiat;
|
||||
}
|
||||
return double.parse(depositAmountFiat).toStringAsPrecision(digits);
|
||||
|
||||
return double.tryParse(depositAmountFiat)?.toStringAsPrecision(digits) ?? '0.00';
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -875,6 +878,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
// 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
|
||||
}
|
||||
} else {
|
||||
printV('calculateBestRate: ${_providers[i].title} returned rate=0');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -914,10 +919,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
to: to,
|
||||
isFixedRateMode: isFixedRateMode,
|
||||
)
|
||||
.onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite))
|
||||
.onError((error, stackTrace) => null)
|
||||
.timeout(
|
||||
Duration(seconds: 7),
|
||||
onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
return MapEntry(provider, limits);
|
||||
}).toList();
|
||||
@@ -925,7 +930,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
final entries = await Future.wait(futures);
|
||||
_providerLimits = Map.fromEntries(entries);
|
||||
|
||||
_providerLimits.values.forEach((tempLimits) {
|
||||
_providerLimits.values
|
||||
.whereType<Limits>()
|
||||
.forEach((tempLimits) {
|
||||
if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin!) {
|
||||
lowestMin = tempLimits.min;
|
||||
}
|
||||
@@ -968,18 +975,58 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
}
|
||||
}
|
||||
|
||||
late final Map<double, ExchangeProvider> providers;
|
||||
if (depositCurrency == CryptoCurrency.btcln &&
|
||||
depositAddress == wallet.walletAddresses.addressForExchange) {
|
||||
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
|
||||
if (invoice != null) {
|
||||
depositAddress = invoice;
|
||||
}
|
||||
}
|
||||
|
||||
if (receiveCurrency == CryptoCurrency.btcln &&
|
||||
receiveAddress == wallet.walletAddresses.addressForExchange) {
|
||||
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
|
||||
if (invoice != null) {
|
||||
receiveAddress = invoice;
|
||||
}
|
||||
}
|
||||
|
||||
Map<double, ExchangeProvider> providers;
|
||||
if (forcedProvider != null) {
|
||||
providers = {forcedProviderRate: forcedProvider!};
|
||||
} else {
|
||||
providers = _sortedAvailableProviders;
|
||||
providers = Map.fromEntries(
|
||||
_sortedAvailableProviders.entries.where(
|
||||
(e) => selectedProviders.contains(e.value),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure we have providers available before attempting to create trade
|
||||
if (providers.isEmpty) {
|
||||
await calculateBestRate();
|
||||
if (forcedProvider != null) {
|
||||
providers = {forcedProviderRate: forcedProvider!};
|
||||
} else {
|
||||
providers = Map.fromEntries(
|
||||
_sortedAvailableProviders.entries.where(
|
||||
(e) => selectedProviders.contains(e.value),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (providers.isEmpty) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: null,
|
||||
function: 'createTrade',
|
||||
error: 'No providers available for $depositCurrency->$receiveCurrency',
|
||||
requestData: {
|
||||
'from': depositCurrency.title,
|
||||
'to': receiveCurrency.title,
|
||||
'fromAmount': _depositAmount,
|
||||
'toAmount': _receiveAmount,
|
||||
},
|
||||
);
|
||||
tradeState = TradeIsCreatedFailure(
|
||||
title: S.current.trade_not_created,
|
||||
error: S.current.none_of_selected_providers_can_exchange);
|
||||
@@ -1022,6 +1069,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
final provider = providersSnapshot[i];
|
||||
final providerRate = ratesSnapshot[i];
|
||||
|
||||
printV('createTrade: trying provider=${provider.title}');
|
||||
|
||||
// Skip Swaps.xyz when sending from external
|
||||
if (isSendFromExternal &&
|
||||
provider.description == ExchangeProviderDescription.swapsXyz) {
|
||||
@@ -1055,7 +1104,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
amount = amount.replaceAll(',', '.');
|
||||
|
||||
if (limitsState is LimitsLoadedSuccessfully) {
|
||||
if (double.tryParse(amount) == null) continue;
|
||||
if (double.tryParse(amount) == null) {
|
||||
printV('createTrade: ${provider.title} amount parse failed: "$amount"');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (limits.min != null && double.parse(amount) < limits.min!) {
|
||||
continue;
|
||||
@@ -1075,6 +1127,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
|
||||
final canCreateTrade = await isCanCreateTrade(trade);
|
||||
if (!canCreateTrade.result) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: provider.description,
|
||||
function: 'createTrade',
|
||||
error: canCreateTrade.errorMessage ?? 'isCanCreateTrade returned false',
|
||||
requestData: {
|
||||
'from': depositCurrency.title,
|
||||
'to': receiveCurrency.title,
|
||||
'fromAmount': _depositAmount,
|
||||
'toAmount': _receiveAmount,
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1084,7 +1147,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
|
||||
/// return after the first successful trade
|
||||
return;
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: provider.description,
|
||||
function: 'createTrade',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': depositCurrency.title,
|
||||
'to': receiveCurrency.title,
|
||||
'fromAmount': _depositAmount,
|
||||
'toAmount': _receiveAmount,
|
||||
'toAddress': receiveAddress,
|
||||
'refundAddress': depositAddress,
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1206,6 +1283,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
_receiveAmount = '';
|
||||
bestRate = 0.0;
|
||||
bestRateProvider = null;
|
||||
_sortedAvailableProviders.clear();
|
||||
loadLimits();
|
||||
_setAvailableProviders();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user