Merge branch 'cake-tech:dev' into dev

This commit is contained in:
BSN ∞/21M
2026-03-19 07:10:33 +01:00
committed by GitHub
29 changed files with 380 additions and 222 deletions
+20
View File
@@ -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
+12 -3
View File
@@ -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]
-1
View File
@@ -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;
}
+2 -1
View File
@@ -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,
};
}
}
+1 -1
View File
@@ -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,
+4 -1
View File
@@ -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]));
+4 -1
View File
@@ -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,
),
),
+10 -9
View File
@@ -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();
}