Files
hash-wallet/lib/view_model/exchange/exchange_trade_view_model.dart
T
jwinterm dd449f2594 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.
2026-10-24 23:49:29 -04:00

333 lines
11 KiB
Dart

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';
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart';
import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:hash_wallet/exchange/trade.dart';
import 'package:hash_wallet/generated/i18n.dart';
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';
import 'package:hash_wallet/view_model/send/output.dart';
import 'package:hash_wallet/view_model/send/send_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
part 'exchange_trade_view_model.g.dart';
class ExchangeTradeViewModel = ExchangeTradeViewModelBase with _$ExchangeTradeViewModel;
abstract class ExchangeTradeViewModelBase with Store {
ExchangeTradeViewModelBase({
required this.wallet,
required this.tradesStore,
required this.sendViewModel,
required this.feesViewModel,
required this.fiatConversionStore,
}) : trade = tradesStore.trade!,
isSwapsXYZCanSendFromExternal =
_checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet) {
setUpOutput();
// Trades from providers whose implementations were removed keep a null
// _provider; status polling is simply skipped for them.
switch (trade.provider) {
case ExchangeProviderDescription.trocador:
_provider = TrocadorExchangeProvider();
break;
case ExchangeProviderDescription.nearIntents:
_provider = NearIntentsExchangeProvider();
break;
}
if (_provider != null) {
_updateTrade();
timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateTrade());
}
}
final WalletBase wallet;
final TradesStore tradesStore;
final SendViewModel sendViewModel;
final FeesViewModel feesViewModel;
late Output output;
AmountParsingProxy get _amountParsingProxy => sendViewModel.amountParsingProxy;
@observable
Trade trade;
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;
if (!isSwapsXYZCanSendFromExternal) return true;
return false;
}
String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty
? "\n\n${S.current.exchange_extra_info}"
: "";
@computed
String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled
? ""
: "${sendViewModel.pendingTransactionFiatAmount} ${sendViewModel.fiat.title}";
@computed
String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled
? ""
: "${sendViewModel.pendingTransactionFeeFiatAmount} ${sendViewModel.fiat.title}";
ExchangeProvider? _provider;
Timer? timer;
final FiatConversionStore fiatConversionStore;
FiatCurrency get fiat => sendViewModel.fiat;
@computed
bool get isFiatDisabled => feesViewModel.isFiatDisabled;
@action
String getReceiveAmountFiatFormatted(String receiveAmount) {
var amount = '0.00';
try {
if (receiveAmount.isNotEmpty) {
if (fiatConversionStore.prices[trade.to] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[trade.to]!,
cryptoAmount: receiveAmount,
);
}
} catch (_) {
printV('Error calculating receive amount fiat formatted: $_');
}
return isFiatDisabled ? '' : '$amount ${fiat.title}';
}
@computed
String get sendAmountFiatFormatted {
var amount = '0.00';
try {
if (trade.amount.isNotEmpty) {
if (fiatConversionStore.prices[trade.from] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[trade.from]!,
cryptoAmount: trade.amount,
);
}
} catch (_) {
printV('Error calculating send amount fiat formatted: $_');
}
return isFiatDisabled ? '' : '$amount ${fiat.title}';
}
void setUpOutput() {
sendViewModel.clearOutputs();
output = sendViewModel.outputs.first;
output.address = trade.inputAddress ?? '';
output.setCryptoAmount(trade.amount);
if (trade.isSendAll == true) output.sendAll = true;
}
@action
Future<void> confirmSending() async {
final canSendError = checkIfCanSend(trade, wallet);
if (canSendError != null) {
_logCanSendError(trade, wallet, canSendError);
sendViewModel.state = FailureState(canSendError);
return;
}
final selected = trade.from;
if (selected == null) {
printV('No selectable currency for trade ${trade.id}');
return;
}
sendViewModel.selectedCryptoCurrency = selected;
await sendViewModel.createTransaction(provider: _provider, trade: trade);
}
@action
Future<void> _updateTrade() async {
try {
final updatedTrade = await _provider!.findTradeById(id: trade.id);
trade.mergeFindTradeByIdResult(updatedTrade);
await trade.save();
tradesStore.setTrade(trade);
} catch (e) {
printV(e.toString());
}
}
String? checkIfCanSend(Trade? trade, WalletBase wallet) {
if (trade == null) return 'Trade is null';
final tradeFrom = trade.from;
if (tradeFrom == null) return 'Trade from currency is null';
bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b);
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 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 (!canSend) {
return 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.';
}
return null;
}
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) {
final provider = trade.provider;
if (provider == ExchangeProviderDescription.swapsXyz && isEVMCompatibleChain(wallet.type)) {
if (trade.routerData != null && trade.routerData != '0x') {
return false;
}
}
return true;
}
PaymentURI? get paymentUri {
final inputAddress = trade.inputAddress;
final amount = trade.amount;
final fromCurrency = trade.from;
if (inputAddress == null || inputAddress.isEmpty || fromCurrency == null) {
return null;
}
// 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);
switch (uriWalletType) {
case WalletType.bitcoin:
return BitcoinURI(address: inputAddress, amount: amount);
case WalletType.bitcoinCash:
return BitcoinCashURI(address: inputAddress, amount: amount);
case WalletType.dogecoin:
return DogeURI(address: inputAddress, amount: amount);
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
return _createERC681URI(fromCurrency, inputAddress, amount);
case WalletType.monero:
return MoneroURI(address: inputAddress, amount: amount);
case WalletType.wownero:
return MoneroURI(address: inputAddress, amount: amount);
case WalletType.litecoin:
return LitecoinURI(amount: amount, address: inputAddress);
case WalletType.nano:
return NanoURI(amount: amount, address: inputAddress);
case WalletType.banano:
case WalletType.none:
return null;
}
}
@action
PaymentURI? _createERC681URI(CryptoCurrency currency, String address, String amount) {
final chainId = TokenUtilities.getChainId(currency);
final isNativeToken = TokenUtilities.isNativeToken(currency);
if (isNativeToken) {
return ERC681URI(
chainId: chainId,
address: address,
amount: amount,
contractAddress: null,
);
} else {
final erc20Token = TokenUtilities.findErc20Token(currency, wallet) ??
TokenUtilities.findErc20TokenForSwap(currency);
if (erc20Token != null) {
return ERC681URI(
chainId: chainId,
address: address,
amount: amount,
contractAddress: erc20Token.contractAddress,
tokenDecimals: erc20Token.decimal,
);
}
return null;
}
}
@computed
String get qrImage => getQrImage(wallet.type);
}