import 'dart:async'; import 'package:hash_wallet/core/amount_parsing_proxy.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/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!, isSendable = _checkIfCanSend(tradesStore, wallet), isSwapsXYZCanSendFromExternal = _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet), items = ObservableList() { 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; } _updateItems(); 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; @observable bool isSendable; bool isSwapsXYZCanSendFromExternal; /// 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}"; @observable ObservableList items; 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 confirmSending() async { if (!isSendable) 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 _updateTrade() async { try { final updatedTrade = await _provider!.findTradeById(id: trade.id); trade.mergeFindTradeByIdResult(updatedTrade); await trade.save(); tradesStore.setTrade(trade); _updateItems(); } catch (e) { printV(e.toString()); } } void _updateItems() { final tradeFrom = trade.from; final tradeTo = trade.to; final tagFrom = tradeFrom?.tag != null ? "${tradeFrom!.tag} " : ""; final tagTo = tradeTo?.tag != null ? "${tradeTo!.tag} " : ""; 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), ); } final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty; 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, ), ); } items.add( ExchangeTradeItem( title: "${S.current.arrive_in_this_address("${tradeTo}", tagTo)}:", data: trade.payoutAddress ?? "", isCopied: true, isReceiveDetail: true, isExternalSendDetail: false, ), ); } 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(); } 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 trade currency here so the external-send QR encodes the correct scheme final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency) ?? wallet.type; 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: 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); 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, ); } return null; } } @computed String get qrImage => getQrImage(wallet.type); }