Merge upstream Cake Wallet v6.5.0

Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:hash_wallet/core/amount_parsing_proxy.dart';
import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
@@ -13,6 +14,7 @@ import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:hash_wallet/store/dashboard/trades_store.dart';
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
import 'package:hash_wallet/utils/qr_util.dart';
import 'package:hash_wallet/utils/token_utilities.dart';
import 'package:hash_wallet/view_model/send/fees_view_model.dart';
@@ -37,10 +39,8 @@ abstract class ExchangeTradeViewModelBase with Store {
required this.feesViewModel,
required this.fiatConversionStore,
}) : trade = tradesStore.trade!,
isSendable = _checkIfCanSend(tradesStore, wallet),
isSwapsXYZCanSendFromExternal =
_checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet),
items = ObservableList<ExchangeTradeItem>() {
_checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet) {
setUpOutput();
// Trades from providers whose implementations were removed keep a null
// _provider; status polling is simply skipped for them.
@@ -53,7 +53,6 @@ abstract class ExchangeTradeViewModelBase with Store {
break;
}
_updateItems();
if (_provider != null) {
_updateTrade();
@@ -73,11 +72,10 @@ abstract class ExchangeTradeViewModelBase with Store {
@observable
Trade trade;
@observable
bool isSendable;
bool isSwapsXYZCanSendFromExternal;
bool get isSendable => checkIfCanSend(trade, wallet) == null;
/// Returns true if the current provider should hide the external send button
bool get shouldHideExternalSendButton {
if (_provider == null) return false;
@@ -101,8 +99,6 @@ abstract class ExchangeTradeViewModelBase with Store {
? ""
: "${sendViewModel.pendingTransactionFeeFiatAmount} ${sendViewModel.fiat.title}";
@observable
ObservableList<ExchangeTradeItem> items;
ExchangeProvider? _provider;
@@ -161,7 +157,13 @@ abstract class ExchangeTradeViewModelBase with Store {
@action
Future<void> confirmSending() async {
if (!isSendable) return;
final canSendError = checkIfCanSend(trade, wallet);
if (canSendError != null) {
_logCanSendError(trade, wallet, canSendError);
sendViewModel.state = FailureState(canSendError);
return;
}
final selected = trade.from;
if (selected == null) {
@@ -183,150 +185,60 @@ abstract class ExchangeTradeViewModelBase with Store {
await trade.save();
tradesStore.setTrade(trade);
_updateItems();
} catch (e) {
printV(e.toString());
}
}
void _updateItems() {
String? checkIfCanSend(Trade? trade, WalletBase wallet) {
if (trade == null) return 'Trade is null';
final tradeFrom = trade.from;
final tradeTo = trade.to;
if (tradeFrom == null) return 'Trade from currency is null';
final tagFrom = tradeFrom?.tag != null ? "${tradeFrom!.tag} " : "";
final tagTo = tradeTo?.tag != null ? "${tradeTo!.tag} " : "";
bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b);
items.clear();
if (trade.provider != ExchangeProviderDescription.thorChain)
items.add(
ExchangeTradeItem(
title: "${trade.provider.title} ${S.current.id}",
data: "${trade.id}",
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
);
if (tradeFrom != null && tradeTo != null) {
items.addAll([
ExchangeTradeItem(
title: S.current.amount,
data:
"${_amountParsingProxy.getDisplayCryptoAmount(trade.amount, tradeFrom)} ${_amountParsingProxy.getCryptoSymbol(tradeFrom)}",
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true,
),
ExchangeTradeItem(
title: "${S.current.you_will_receive_estimated_amount}:",
data:
"${_amountParsingProxy.getDisplayCryptoAmount(trade.receiveAmount ?? "0", tradeTo)} ${_amountParsingProxy.getCryptoSymbol(tradeTo)}",
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
ExchangeTradeItem(
title: "${S.current.send_to_this_address("$tradeFrom", tagFrom)}:",
data: trade.inputAddress ?? '',
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true,
),
]);
items.add(
isSwapsXYZCanSendFromExternal
? ExchangeTradeItem(
title: S.current.send_to_this_address('${tradeFrom}', tagFrom) + ':',
data: trade.inputAddress ?? '',
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true)
: ExchangeTradeItem(
title: 'Smart contract call (no address required)',
data: 'Wallet will execute a contract call. On-chain transaction',
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true),
);
bool _isTokenBelongingToWallet(CryptoCurrency cur) {
final chainTag = cur.tag ?? cur.title;
return wallet.currency == cur &&
(tradeFrom.tag?.toUpperCase() == chainTag.toUpperCase() ||
tradeFrom.title.toUpperCase() == chainTag.toUpperCase());
}
final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty;
final canSend = _sameCurrency(tradeFrom, wallet.currency) ||
(_sameCurrency(tradeFrom, CryptoCurrency.btcln) && wallet.currency == CryptoCurrency.btc) ||
trade.provider == ExchangeProviderDescription.xmrto ||
_isTokenBelongingToWallet(CryptoCurrency.eth) ||
_isTokenBelongingToWallet(CryptoCurrency.maticpoly) ||
_isTokenBelongingToWallet(CryptoCurrency.baseEth) ||
_isTokenBelongingToWallet(CryptoCurrency.arbEth) ||
_isTokenBelongingToWallet(CryptoCurrency.trx) ||
_isTokenBelongingToWallet(CryptoCurrency.sol) ||
_isTokenBelongingToWallet(CryptoCurrency.bnb);
if (isExtraIdExist) {
final title = tradeFrom == CryptoCurrency.xrp
? S.current.destination_tag
: [CryptoCurrency.xlm, CryptoCurrency.ton].contains(tradeFrom)
? S.current.memo
: S.current.extra_id;
items.add(
ExchangeTradeItem(
title: title,
data: trade.extraId ?? "",
isCopied: true,
isReceiveDetail: !isExtraIdExist,
isExternalSendDetail: isExtraIdExist,
),
);
if (!canSend) {
return 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.';
}
items.add(
ExchangeTradeItem(
title: "${S.current.arrive_in_this_address("${tradeTo}", tagTo)}:",
data: trade.payoutAddress ?? "",
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
);
return null;
}
static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) {
final trade = tradesStore.trade!;
final tradeFrom = trade.from;
bool _sameCurrency(CryptoCurrency? a, CryptoCurrency? b) =>
a != null && b != null && a.titleAndTagEqual(b);
bool _isEthToken() =>
wallet.currency == CryptoCurrency.eth && tradeFrom?.tag == CryptoCurrency.eth.title;
bool _isPolygonToken() =>
wallet.currency == CryptoCurrency.maticpoly &&
tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
bool _isBaseToken() =>
wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag;
bool _isArbitrumToken() =>
wallet.currency == CryptoCurrency.arbEth &&
(tradeFrom?.tag == CryptoCurrency.arbEth.tag ||
tradeFrom?.title ==
CryptoCurrency.arbEth
.tag); // This is to handle the CryptoCurrency.arb that doesn't have a tag but fully belongs to the Arbitrum chain
bool _isTronToken() =>
wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title;
bool _isSplToken() =>
wallet.currency == CryptoCurrency.sol && tradeFrom?.tag == CryptoCurrency.sol.title;
bool _isBscToken() =>
wallet.currency == CryptoCurrency.bnb && tradeFrom?.tag == CryptoCurrency.bnb.tag;
return _sameCurrency(tradeFrom, wallet.currency) ||
(_sameCurrency(tradeFrom, CryptoCurrency.btcln) && wallet.currency == CryptoCurrency.btc) ||
tradesStore.trade!.provider == ExchangeProviderDescription.xmrto ||
_isEthToken() ||
_isPolygonToken() ||
_isSplToken() ||
_isTronToken() ||
_isBaseToken() ||
_isArbitrumToken() ||
_isBscToken();
void _logCanSendError(Trade? trade, WalletBase wallet, String error) {
ExchangeProviderLogger.logError(
provider: trade?.provider,
function: '_checkIfCanSend',
error: error,
requestData: {
'tradeId': trade?.id,
'tradeFrom': trade?.from?.title,
'tradeFromTag': trade?.from?.tag,
'tradeTo': trade?.to?.title,
'tradeToTag': trade?.to?.tag,
'walletName': wallet.name,
'walletCurrency': wallet.currency.title,
'walletCurrencyTag': wallet.currency.tag,
},
);
}
static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) {
@@ -349,8 +261,13 @@ abstract class ExchangeTradeViewModelBase with Store {
return null;
}
// Using trade currency here so the external-send QR encodes the correct scheme
final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency) ?? wallet.type;
// Using the trade's `from` currency so the external-send QR encodes the correct scheme.
final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency);
// for other currencies that we don't have wallets for
if (uriWalletType == null) {
return ExternalAddressURI(address: inputAddress, amount: amount);
}
printV(uriWalletType);
@@ -362,16 +279,11 @@ abstract class ExchangeTradeViewModelBase with Store {
case WalletType.dogecoin:
return DogeURI(address: inputAddress, amount: amount);
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.base:
return BaseURI(amount: amount, address: inputAddress);
case WalletType.arbitrum:
return ArbitrumURI(amount: amount, address: inputAddress);
case WalletType.bsc:
return BSCURI(amount: amount, address: inputAddress);
return _createERC681URI(fromCurrency, inputAddress, amount);
case WalletType.monero:
return MoneroURI(address: inputAddress, amount: amount);
case WalletType.wownero:
@@ -408,6 +320,7 @@ abstract class ExchangeTradeViewModelBase with Store {
address: address,
amount: amount,
contractAddress: erc20Token.contractAddress,
tokenDecimals: erc20Token.decimal,
);
}
return null;
+209 -139
View File
@@ -28,16 +28,16 @@ import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:hash_wallet/exchange/trade.dart';
import 'package:hash_wallet/exchange/trade_request.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
import 'package:hash_wallet/utils/show_pop_up.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/amount/money_double.dart';
import "package:cw_core/wallet_info.dart";
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:hash_wallet/store/dashboard/trades_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/store/templates/exchange_template_store.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/utils/token_utilities.dart';
@@ -46,6 +46,7 @@ import 'package:hash_wallet/view_model/send/fees_view_model.dart';
import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currencies_with_memo.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/sync_status.dart';
@@ -83,7 +84,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
ExchangeViewModelBase(
this._appStore,
this._exchangeTemplateStore,
this.tradesStore,
this.sharedPreferences,
this.contactListViewModel,
@@ -93,8 +93,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
) : isSendAllEnabled = false,
isFixedRateMode = false,
isReceiveAmountEntered = false,
_depositAmount = '',
_receiveAmount = '',
_depositAmount = null,
_receiveAmount = null,
receiveAddress = '',
depositAddress = '',
isDepositAddressEnabled = false,
@@ -192,8 +192,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
});
isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
receiveAddress = '';
depositAddress =
useSameWalletAddress(depositCurrency) ? wallet.walletAddresses.addressForExchange : '';
@@ -261,12 +261,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
final balance = wallet.balance[depositCurrency];
if (balance != null) {
depositAvailableAmount = _appStore.amountParsingProxy
.getDisplayCryptoStringFromBigInt(balance.fullAvailableBalance, depositCurrency);
depositAvailableAmount = _appStore.amountParsingProxy.asDisplayString(balance.available);
return false;
}
return true;
});
}).timeout(Duration(seconds: 20));
} else if (isEVMCompatibleChain(wallet.type)) {
final currency = depositCurrency;
final balanceCurrency = wallet.balance.keys.firstWhereOrNull(
@@ -276,14 +275,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
);
final balanceForCurrency = balanceCurrency != null ? wallet.balance[balanceCurrency] : null;
if (depositCurrency == currency && balanceForCurrency != null) {
depositAvailableAmount = _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(
balanceForCurrency.fullAvailableBalance, balanceCurrency!);
depositAvailableAmount =
_appStore.amountParsingProxy.asDisplayStringWithSymbol(balanceForCurrency.available);
}
} else {
final currency = depositCurrency;
final amount = _appStore.amountParsingProxy.getDisplayCryptoString(
(await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any)),
depositCurrency);
final sendingBalance = Money.fromInt(
await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any), currency);
final amount = _appStore.amountParsingProxy.asDisplayStringWithSymbol(sendingBalance);
if (depositCurrency == currency) {
depositAvailableAmount = amount;
}
@@ -291,17 +290,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
void showFiatCurrencyPicker(BuildContext context) {
showPopUp<void>(
FiatCurrencyPickerSheet.show(
context: context,
builder: (_) => CurrencyPicker(
key: ValueKey('send_page_currency_picker_dialog_button_key'),
selectedAtIndex: FiatCurrency.all.indexOf(fiat),
items: FiatCurrency.all,
hintText: S.of(context).search_currency,
onItemSelected: (Currency cur) async {
_settingsStore.fiatCurrency = (cur as FiatCurrency);
},
),
selected: fiat,
onSelected: (cur) => _settingsStore.fiatCurrency = cur,
);
}
@@ -321,10 +313,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
].contains(wallet.type);
bool get hideAddressAfterExchange =>
[WalletType.monero, WalletType.wownero].contains(wallet.type);
[WalletType.monero, WalletType.wownero].contains(wallet.type) || isElectrumWallet;
bool _useTorOnly;
final ExchangeTemplateStore _exchangeTemplateStore;
final TradesStore tradesStore;
final SharedPreferences sharedPreferences;
@@ -378,27 +369,29 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
ExchangeTradeState tradeState;
@observable
String _depositAmount;
Money? _depositAmount;
@computed
String get depositAmount => _depositAmount == "..."
? _depositAmount
: amountParsingProxy.getDisplayCryptoAmount(_depositAmount, depositCurrency);
bool get hasDepositAmount => _depositAmount != null;
@computed
String get depositAmountCanonical => _depositAmount;
String get depositAmount =>
_depositAmount == null ? "" : amountParsingProxy.asDisplayString(_depositAmount!);
@computed
String get depositAmountCanonical => _depositAmount == null ? "0.0" : _depositAmount.toString();
@observable
String _receiveAmount;
Money? _receiveAmount;
@computed
String get receiveAmount => _receiveAmount == "..."
? _receiveAmount
: amountParsingProxy.getDisplayCryptoAmount(_receiveAmount, receiveCurrency);
String get receiveAmount =>
_receiveAmount == null ? "" : amountParsingProxy.asDisplayString(_receiveAmount!);
@action
// only set canonical formated amounts here;
void setCanonicalReceiveAmount(String value) => _receiveAmount = value;
void setCanonicalReceiveAmount(String value) =>
_receiveAmount = Money.tryParse(value, receiveCurrency);
@observable
String depositAddress;
@@ -406,6 +399,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@observable
String receiveAddress;
@observable
String receiveAddressExtraId = '';
@observable
String? receiveAddressDisplayName;
@@ -430,14 +426,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@observable
bool tradeStarted = false;
@observable
bool noProviderForPair = false;
@observable
bool isSendFromExternal = false;
@computed
SyncStatus get status => wallet.syncStatus;
@computed
ObservableList<ExchangeTemplate> get templates => _exchangeTemplateStore.templates;
@computed
List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts
@@ -451,14 +448,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@computed
Future<List<WalletInfo>> get receiveWallets async {
WalletType? type;
type = cryptoCurrencyOrTokenToWalletType(receiveCurrency);
if (type == null) {
type =
cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? ""));
}
try {
WalletType? type;
type = cryptoCurrencyOrTokenToWalletType(receiveCurrency);
if (type == null) {
type =
cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? ""));
}
return await WalletInfo.selectList("type = ?", [type!.index]);
return await WalletInfo.selectList("type = ?", [type!.index]);
} catch (e) {
return [];
}
}
Future<List<WalletInfoAddressInfo>> addressesForAccountsWallet(WalletInfo wallet) async {
@@ -517,11 +518,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
} else {
balanceCurrency = depositCurrency;
}
final bal =
balanceCurrency != null ? wallet.balance[balanceCurrency]?.fullAvailableBalance : null;
final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.available : null;
if (bal == null) return null;
return amountParsingProxy.getDisplayCryptoStringFromBigInt(
bal, balanceCurrency ?? depositCurrency);
return amountParsingProxy.asDisplayString(bal);
}
//* Still open to further optimize these checks
@@ -596,10 +595,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
double bestRate = 0.0;
@computed
bool get useSatoshiDeposit => _appStore.amountParsingProxy.useSatoshi(depositCurrency);
bool get useDepositBaseUnit => _appStore.amountParsingProxy.useSatoshi(depositCurrency);
@computed
bool get useSatoshisReceive => _appStore.amountParsingProxy.useSatoshi(receiveCurrency);
bool get useReceiveBaseUnit => _appStore.amountParsingProxy.useSatoshi(receiveCurrency);
@computed
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
@@ -648,12 +647,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
String get receiveAmountFiatFormatted {
var amount = '0.00';
try {
if (_receiveAmount.isNotEmpty) {
if (_receiveAmount != null) {
if (fiatConversionStore.prices[receiveCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[receiveCurrency]!,
cryptoAmount: _receiveAmount,
cryptoAmount: _receiveAmount.toString(),
);
}
} catch (_) {
@@ -666,12 +665,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
String get depositAmountFiatFormatted {
var amount = '0.00';
try {
if (_depositAmount.isNotEmpty) {
if (_depositAmount != null) {
if (fiatConversionStore.prices[depositCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[depositCurrency]!,
cryptoAmount: _depositAmount,
cryptoAmount: _depositAmount.toString(),
);
}
} catch (_) {
@@ -684,12 +683,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
String get receiveAmountFiat {
var amount = '';
try {
if (_receiveAmount.isNotEmpty) {
if (_receiveAmount != null) {
if (fiatConversionStore.prices[receiveCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[receiveCurrency]!,
cryptoAmount: _receiveAmount,
cryptoAmount: _receiveAmount.toString(),
raw: true);
}
} catch (_) {
@@ -702,13 +701,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
String get depositAmountFiat {
var amount = '';
try {
if (_depositAmount.isNotEmpty) {
if (_depositAmount != null) {
if (fiatConversionStore.prices[depositCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[depositCurrency]!,
cryptoAmount: _depositAmount,
raw: true);
price: fiatConversionStore.prices[depositCurrency]!,
cryptoAmount: _depositAmount.toString(),
raw: true,
);
}
} catch (_) {
log('Error calculating deposit amount fiat formatted: $_');
@@ -780,6 +780,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
currency == receiveCurrency)) {
receiveAddress = "";
}
if (currency != receiveCurrency) {
receiveAddressExtraId = "";
}
receiveCurrency = currency;
isFixedRateMode = false;
@@ -792,32 +795,37 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
@action
Future<void> changeReceiveAmount({required String amount}) async {
amount = amountParsingProxy.getCanonicalCryptoAmount(amount, receiveCurrency);
_receiveAmount = amount;
Future<void> changeReceiveAmount({required String amount, bool isCanonical = false}) async {
if (amount.isEmpty) {
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
return;
}
final _enteredAmount = double.tryParse(_receiveAmount.replaceAll(',', '.')) ?? 0;
_receiveAmount = isCanonical
? Money.tryParse(amount.sanitized(), receiveCurrency)
: _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), receiveCurrency);
if (_receiveAmount == null) {
_depositAmount = null;
return;
}
final _enteredAmount = double.tryParse(_receiveAmount.toString()) ?? 0;
if (bestRate == 0) {
_depositAmount = "...";
_depositAmount = null;
await calculateBestRate();
}
final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate);
_depositAmount = amount_.toString().withMaxDecimals(depositCurrency.decimals);
_depositAmount = amount_.tryToMoney(depositCurrency);
}
@action
void setReceiveAmountFromFiat({required String fiatAmount}) {
final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0;
final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0;
final price = fiatConversionStore.prices[receiveCurrency];
if (price == null || price == 0.0) return;
@@ -830,7 +838,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
void setDepositAmountFromFiat({required String fiatAmount}) {
final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0;
final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0;
final price = fiatConversionStore.prices[depositCurrency];
if (price == null || price == 0.0) return;
@@ -843,14 +851,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
Future<void> changeDepositAmount({required String amount, bool isCanonical = false}) async {
_depositAmount = isCanonical
? amount.replaceAll(',', '.')
: _appStore.amountParsingProxy
.getCanonicalCryptoAmount(amount.replaceAll(',', '.'), depositCurrency);
if (amount.isEmpty) {
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
return;
}
_depositAmount = isCanonical
? Money.tryParse(amount.sanitized(), depositCurrency)
: _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), depositCurrency);
if (_depositAmount == null) {
_receiveAmount = null;
return;
}
@@ -858,21 +870,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
/// as it should remain exactly what the user set
if (isFixedRateMode) return;
final _enteredAmount = double.tryParse(_depositAmount) ?? 0;
final _enteredAmount = double.tryParse(_depositAmount.toString()) ?? 0;
/// in case the best rate was not calculated yet
if (bestRate == 0) {
_receiveAmount = "...";
_receiveAmount = null;
await calculateBestRate();
}
final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate);
_receiveAmount = amount_.toString().withMaxDecimals(receiveCurrency.decimals);
_receiveAmount = amount_.tryToMoney(receiveCurrency);
}
bool checkIfInputMeetsMinOrMaxCondition(String input) {
final _enteredAmount = double.tryParse(input.replaceAll(',', '.')) ?? 0;
final _enteredAmount = double.tryParse(input.sanitized()) ?? 0;
double minLimit = limits.min ?? 0;
double? maxLimit = limits.max;
@@ -889,8 +901,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return;
}
final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final amount =
double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
forcedProviderRate = await forcedProvider!.fetchRate(
from: depositCurrency,
@@ -905,17 +918,22 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
(provider.description == ExchangeProviderDescription.swapsXyz ||
provider.description == ExchangeProviderDescription.nearIntents);
bool _excludeProviderForReceiveExtraId(ExchangeProvider provider) =>
memoLabelTypeFor(receiveCurrency) != null && !provider.supportsMemoOrDestinationTag;
Future<void> calculateBestRate() async {
if (depositCurrency == receiveCurrency) {
bestRate = 0.0;
bestRateProvider = null;
return;
}
final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final amount =
double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
if (_excludeProviderForSwapAll(provider)) return false;
if (_excludeProviderForReceiveExtraId(provider)) return false;
final limits = _providerLimits[provider];
@@ -972,6 +990,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bestRate = _sortedAvailableProviders.keys.first;
bestRateProvider = _sortedAvailableProviders.values.first;
}
noProviderForPair = _sortedAvailableProviders.isEmpty;
}
@action
@@ -993,6 +1012,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
try {
final futures = selectedProviders
.where((provider) => providerList.contains(provider))
.where((provider) => !_excludeProviderForReceiveExtraId(provider))
.map((provider) async {
final limits = await provider
.fetchLimits(
@@ -1041,19 +1061,73 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
Future<void> createTrade() async {
// Ignore repeated taps while a trade is already being created
if (tradeState is TradeIsCreating) return;
// Set synchronously, before the first await, so the swap button shows its
// loading state and further taps are ignored right away
tradeState = TradeIsCreating();
try {
await _createTrade();
} finally {
// Never leave the swap button stuck in its loading state
// (e.g. if an unexpected exception is thrown)
if (tradeState is TradeIsCreating) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange);
}
}
}
Future<void> _createTrade() async {
final depositAmountValue = _depositAmount ?? Money.zero(depositCurrency);
final receiveAmountValue = _receiveAmount ?? Money.zero(receiveCurrency);
if (depositAmountValue.isZero ||
depositAmountValue.isNegative ||
receiveAmountValue.isZero ||
receiveAmountValue.isNegative) {
ExchangeProviderLogger.logError(
provider: forcedProvider?.description ?? bestRateProvider?.description,
function: 'createTrade',
error: 'Invalid swap amount: deposit=$_depositAmount receive=$_receiveAmount',
requestData: {
'from': depositCurrency.title,
'to': receiveCurrency.title,
'fromAmount': _depositAmount,
'toAmount': _receiveAmount,
'isFixedRateMode': isFixedRateMode,
'forcedProvider': forcedProvider?.title,
'bestRateProvider': bestRateProvider?.title,
},
);
final invalidAmountError = depositAmountValue.isZero || depositAmountValue.isNegative
? '$depositAmountValue is not a valid amount for depositAmount'
: '$receiveAmountValue is not a valid amount for receiveAmount';
tradeState =
TradeIsCreatedFailure(title: S.current.trade_not_created, error: invalidAmountError);
return;
}
if (isSendAllEnabled) {
await calculateDepositAllAmount();
final amount = double.tryParse(_depositAmount);
final amount = double.tryParse(_depositAmount.toString());
if (limits.min != null && amount != null && amount < limits.min!) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.amount_is_below_minimum_limit(limits.min!.toString()));
title: S.current.trade_not_created,
error: S.current.amount_is_below_minimum_limit(limits.min!.toString()),
);
return;
}
}
if (depositCurrency == CryptoCurrency.btcln &&
wallet.type == WalletType.bitcoin &&
depositAddress == wallet.walletAddresses.addressForExchange) {
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
if (invoice != null) {
@@ -1062,6 +1136,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
if (receiveCurrency == CryptoCurrency.btcln &&
wallet.type == WalletType.bitcoin &&
receiveAddress == wallet.walletAddresses.addressForExchange) {
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
if (invoice != null) {
@@ -1069,6 +1144,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
if (forcedProvider != null && _excludeProviderForReceiveExtraId(forcedProvider!)) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange,
);
return;
}
Map<double, ExchangeProvider> providers;
if (forcedProvider != null) {
providers = {forcedProviderRate: forcedProvider!};
@@ -1158,6 +1241,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
continue;
}
if (_excludeProviderForReceiveExtraId(provider)) {
continue;
}
// Skip Swaps.xyz when sending from external
if (isSendFromExternal && provider.description == ExchangeProviderDescription.swapsXyz) {
printV('Skipping Swaps.xyz for external send');
@@ -1169,15 +1256,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bestRate = providerRate;
bestRateProvider = provider;
await changeDepositAmount(amount: _depositAmount, isCanonical: true);
await changeDepositAmount(amount: _depositAmount.toString(), isCanonical: true);
final request = TradeRequest(
fromCurrency: depositCurrency,
toCurrency: receiveCurrency,
fromAmount: _depositAmount.replaceAll(',', '.'),
toAmount: _receiveAmount.replaceAll(',', '.'),
fromAmount: _depositAmount.toString(),
toAmount: _receiveAmount.toString(),
refundAddress: depositAddress,
toAddress: receiveAddress,
toAddressExtraId: receiveAddressExtraId.trim(),
isFixedRate: isFixedRateMode,
);
@@ -1186,8 +1274,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
await wallet.walletAddresses.saveAddressesInBox();
}
var amount = isFixedRateMode ? _receiveAmount : _depositAmount;
amount = amount.replaceAll(',', '.');
var amount = isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString();
if (limitsState is LimitsLoadedSuccessfully) {
if (double.tryParse(amount) == null) {
@@ -1210,6 +1297,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
trade.walletId = wallet.id;
trade.chainId = wallet.chainId;
trade.fromWalletAddress = wallet.walletAddresses.address;
if (trade.from == null) {
trade.from = depositCurrency;
}
if (trade.to == null) {
trade.to = receiveCurrency;
}
final canCreateTrade = await isCanCreateTrade(trade);
if (!canCreateTrade.result) {
@@ -1264,7 +1357,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
///
/// this is because the limitation of the SplayTreeMap that
/// you can't modify it while iterating through it
Future.delayed(Duration(milliseconds: 200), createTrade);
await Future<void>.delayed(Duration(milliseconds: 200));
await _createTrade();
}
}
@@ -1272,12 +1366,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
void reset() {
_initialPairBasedOnWallet();
isReceiveAmountEntered = false;
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
depositAddress =
depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
receiveAddress =
receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
receiveAddressExtraId = '';
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
isFixedRateMode = false;
_onPairChange();
@@ -1308,8 +1403,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final amount = depositCurrency == CryptoCurrency.btcln
// FIXME amount estimation is broken/impossible for ln, konsti suggested this
? (amountParsingProxy.parseCryptoString(depositAvailableAmount, depositCurrency).toInt() -
10)
? (amountParsingProxy.parseCryptoString(depositAvailableAmount, depositCurrency) -
Money.fromInt(10, depositCurrency))
: await bitcoin!.estimateFakeSendAllTxAmount(
wallet,
priority,
@@ -1320,8 +1415,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
: UnspentCoinType.any,
);
changeDepositAmount(
amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true);
changeDepositAmount(amount: amount.toString(), isCanonical: true);
} else if (wallet.type == WalletType.monero) {
final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any);
@@ -1340,8 +1434,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return;
}
final balanceAmount = _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(
balanceForCurrency.fullAvailableBalance, balanceCurrency!);
final balanceAmount =
_appStore.amountParsingProxy.asDisplayString(balanceForCurrency.available);
final balanceDouble = double.tryParse(balanceAmount.replaceAll(',', '.')) ?? 0.0;
if (balanceDouble <= 0) {
changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true);
@@ -1369,7 +1463,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return;
}
final balanceWei = balanceForCurrency.fullAvailableBalance;
final balanceWei = balanceForCurrency.available.amount;
final feeWei = BigInt.parse(feeString);
final amountAfterFeeWei = balanceWei > feeWei ? balanceWei - feeWei : BigInt.zero;
changeDepositAmount(
@@ -1393,40 +1487,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
_depositAmount = tmpAmount;
}
void updateTemplate() => _exchangeTemplateStore.update();
void addTemplate(
{required String amount,
required String depositCurrency,
required String receiveCurrency,
required String provider,
required String depositAddress,
required String receiveAddress,
required String depositCurrencyTitle,
required String receiveCurrencyTitle}) =>
_exchangeTemplateStore.addTemplate(
amount: amount,
depositCurrency: depositCurrency,
receiveCurrency: receiveCurrency,
provider: provider,
depositAddress: depositAddress,
receiveAddress: receiveAddress,
depositCurrencyTitle: depositCurrencyTitle,
receiveCurrencyTitle: receiveCurrencyTitle);
void removeTemplate({required ExchangeTemplate template}) =>
_exchangeTemplateStore.remove(template: template);
void _onPairChange({bool clearBoth = false}) {
if (clearBoth) {
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
if (!hasAllAmount) isSendAllEnabled = false;
} else {
if (isFixedRateMode) {
_depositAmount = '';
_depositAmount = null;
} else {
_receiveAmount = '';
_receiveAmount = null;
}
}
bestRate = 0.0;
@@ -1527,8 +1597,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
void saveSelectedProviders() {
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
isFixedRateMode = false;
_defineIsReceiveAmountEditable();
bestRateProvider = null;