mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-27 06:18:50 +01:00
500 lines
17 KiB
Dart
500 lines
17 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
|
|
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
|
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
|
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
|
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/trade.dart';
|
|
import 'package:cake_wallet/generated/i18n.dart';
|
|
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
|
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
|
|
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
|
import 'package:cake_wallet/store/dashboard/trades_store.dart';
|
|
import 'package:cake_wallet/utils/qr_util.dart';
|
|
import 'package:cake_wallet/utils/token_utilities.dart';
|
|
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
|
|
import 'package:cake_wallet/view_model/send/output.dart';
|
|
import 'package:cake_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();
|
|
switch (trade.provider) {
|
|
case ExchangeProviderDescription.changeNow:
|
|
_provider =
|
|
ChangeNowExchangeProvider(settingsStore: sendViewModel.balanceViewModel.settingsStore);
|
|
break;
|
|
case ExchangeProviderDescription.sideShift:
|
|
_provider = SideShiftExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.simpleSwap:
|
|
_provider = SimpleSwapExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.trocador:
|
|
_provider = TrocadorExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.exolix:
|
|
_provider = ExolixExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.swapTrade:
|
|
_provider = SwapTradeExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.stealthEx:
|
|
_provider = StealthExExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.thorChain:
|
|
_provider = ThorChainExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.chainflip:
|
|
_provider = ChainflipExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.xoSwap:
|
|
_provider = XOSwapExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.swapsXyz:
|
|
_provider = SwapsXyzExchangeProvider();
|
|
break;
|
|
case ExchangeProviderDescription.jupiter:
|
|
_provider = JupiterExchangeProvider();
|
|
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;
|
|
|
|
/// Providers that should hide the "send from external" button
|
|
static const List<Type> _providersThatHideExternalSend = [
|
|
JupiterExchangeProvider,
|
|
];
|
|
|
|
/// 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 _providersThatHideExternalSend.any(
|
|
(providerType) => _provider.runtimeType == providerType,
|
|
);
|
|
}
|
|
|
|
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 (_provider is ThorChainExchangeProvider) output.memo = trade.memo ?? "";
|
|
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;
|
|
|
|
final pendingTransaction =
|
|
await sendViewModel.createTransaction(provider: _provider, trade: trade);
|
|
|
|
if (_provider is SwapsXyzExchangeProvider) {
|
|
final hash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? '';
|
|
trade.txId = hash;
|
|
await trade.save();
|
|
}
|
|
|
|
if (_provider is ThorChainExchangeProvider) {
|
|
trade.id = pendingTransaction?.id ?? '';
|
|
await trade.save();
|
|
}
|
|
}
|
|
|
|
@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.solana:
|
|
return SolanaURI(amount: amount, address: inputAddress);
|
|
case WalletType.tron:
|
|
return TronURI(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.zano:
|
|
return ZanoURI(amount: amount, address: inputAddress);
|
|
case WalletType.decred:
|
|
return DecredURI(amount: amount, address: inputAddress);
|
|
case WalletType.zcash:
|
|
return ZcashURI(amount: amount, address: inputAddress);
|
|
case WalletType.banano:
|
|
case WalletType.none:
|
|
case WalletType.haven:
|
|
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);
|
|
}
|