Files
hash-wallet/lib/view_model/exchange/exchange_trade_view_model.dart
jwinterm adece963c3 refactor: delete dead Cake Pay/buy/exchange code; split di setup()
- Cake Pay: 36 files (~5.3k LOC) plus DI registrations, routes, and UI
  branches (feature was hard-disabled and unreachable)
- unreachable buy providers: robinhood, dfx, onramper, meld, kryptonim
  (~1.7k LOC); MoonPay untouched
- exchange providers that only existed as commented-out wiring:
  changenow, sideshift, chainflip, exolix, swaptrade, letsexchange,
  stealthex, xoswap, swapsxyz, thorchain, simpleswap (~5.2k LOC);
  Trocador untouched; ExchangeProviderDescription values and Hive
  adapters kept so stored trades and orders still deserialize
- di.dart setup() split from one 1,452-line function into 10 sequential
  chunk functions; registration execution order verified identical
  before and after (251 register calls)
2026-08-15 16:29:03 -04:00

426 lines
14 KiB
Dart

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<ExchangeTradeItem>() {
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<ExchangeTradeItem> 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<void> 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<void> _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);
}