dev
dart 514 lines 17.4 KB
Raw
1 import 'dart:async';
2
3 import 'package:cake_wallet/core/amount_parsing_proxy.dart';
4 import 'package:cake_wallet/core/execution_state.dart';
5 import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
6 import 'package:cake_wallet/entities/fiat_currency.dart';
7 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
8 import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
9 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
10 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
11 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
12 import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
13 import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
14 import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
15 import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
16 import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
17 import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
18 import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
19 import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
20 import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
21 import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
22 import 'package:cake_wallet/exchange/trade.dart';
23 import 'package:cake_wallet/generated/i18n.dart';
24 import 'package:cake_wallet/reactions/wallet_connect.dart';
25 import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
26 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
27 import 'package:cake_wallet/store/dashboard/trades_store.dart';
28 import 'package:cake_wallet/utils/exchange_provider_logger.dart';
29 import 'package:cake_wallet/utils/qr_util.dart';
30 import 'package:cake_wallet/utils/token_utilities.dart';
31 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
32 import 'package:cake_wallet/view_model/send/output.dart';
33 import 'package:cake_wallet/view_model/send/send_view_model.dart';
34 import 'package:cw_core/crypto_currency.dart';
35 import 'package:cw_core/payment_uris.dart';
36 import 'package:cw_core/utils/print_verbose.dart';
37 import 'package:cw_core/wallet_base.dart';
38 import 'package:cw_core/wallet_type.dart';
39 import 'package:mobx/mobx.dart';
40
41 part 'exchange_trade_view_model.g.dart';
42
43 class ExchangeTradeViewModel = ExchangeTradeViewModelBase with _$ExchangeTradeViewModel;
44
45 abstract class ExchangeTradeViewModelBase with Store {
46 ExchangeTradeViewModelBase({
47 required this.wallet,
48 required this.tradesStore,
49 required this.sendViewModel,
50 required this.feesViewModel,
51 required this.fiatConversionStore,
52 }) : trade = tradesStore.trade!,
53 isSwapsXYZCanSendFromExternal =
54 _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet),
55 items = ObservableList<ExchangeTradeItem>() {
56 setUpOutput();
57 switch (trade.provider) {
58 case ExchangeProviderDescription.changeNow:
59 _provider =
60 ChangeNowExchangeProvider(settingsStore: sendViewModel.balanceViewModel.settingsStore);
61 break;
62 case ExchangeProviderDescription.sideShift:
63 _provider = SideShiftExchangeProvider();
64 break;
65 case ExchangeProviderDescription.simpleSwap:
66 _provider = SimpleSwapExchangeProvider();
67 break;
68 case ExchangeProviderDescription.trocador:
69 _provider = TrocadorExchangeProvider();
70 break;
71 case ExchangeProviderDescription.exolix:
72 _provider = ExolixExchangeProvider();
73 break;
74 case ExchangeProviderDescription.swapTrade:
75 _provider = SwapTradeExchangeProvider();
76 break;
77 case ExchangeProviderDescription.stealthEx:
78 _provider = StealthExExchangeProvider();
79 break;
80 case ExchangeProviderDescription.thorChain:
81 _provider = ThorChainExchangeProvider();
82 break;
83 case ExchangeProviderDescription.chainflip:
84 _provider = ChainflipExchangeProvider();
85 break;
86 case ExchangeProviderDescription.xoSwap:
87 _provider = XOSwapExchangeProvider();
88 break;
89 case ExchangeProviderDescription.swapsXyz:
90 _provider = SwapsXyzExchangeProvider();
91 break;
92 case ExchangeProviderDescription.jupiter:
93 _provider = JupiterExchangeProvider();
94 break;
95 case ExchangeProviderDescription.nearIntents:
96 _provider = NearIntentsExchangeProvider();
97 break;
98 }
99
100 _updateItems();
101
102 if (_provider != null) {
103 _updateTrade();
104 timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateTrade());
105 }
106 }
107
108 final WalletBase wallet;
109 final TradesStore tradesStore;
110 final SendViewModel sendViewModel;
111 final FeesViewModel feesViewModel;
112
113 late Output output;
114
115 AmountParsingProxy get _amountParsingProxy => sendViewModel.amountParsingProxy;
116
117 @observable
118 Trade trade;
119
120 bool isSwapsXYZCanSendFromExternal;
121
122 bool get isSendable => checkIfCanSend(trade, wallet) == null;
123
124 /// Providers that should hide the "send from external" button
125 static const List<Type> _providersThatHideExternalSend = [
126 JupiterExchangeProvider,
127 ];
128
129 /// Returns true if the current provider should hide the external send button
130 bool get shouldHideExternalSendButton {
131 if (_provider == null) return false;
132
133 if (!isSwapsXYZCanSendFromExternal) return true;
134
135 return _providersThatHideExternalSend.any(
136 (providerType) => _provider.runtimeType == providerType,
137 );
138 }
139
140 String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty
141 ? "\n\n${S.current.exchange_extra_info}"
142 : "";
143
144 @computed
145 String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled
146 ? ""
147 : "${sendViewModel.pendingTransactionFiatAmount} ${sendViewModel.fiat.title}";
148
149 @computed
150 String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled
151 ? ""
152 : "${sendViewModel.pendingTransactionFeeFiatAmount} ${sendViewModel.fiat.title}";
153
154 @observable
155 ObservableList<ExchangeTradeItem> items;
156
157 ExchangeProvider? _provider;
158
159 Timer? timer;
160
161 final FiatConversionStore fiatConversionStore;
162
163 FiatCurrency get fiat => sendViewModel.fiat;
164
165 @computed
166 bool get isFiatDisabled => feesViewModel.isFiatDisabled;
167
168 @action
169 String getReceiveAmountFiatFormatted(String receiveAmount) {
170 var amount = '0.00';
171 try {
172 if (receiveAmount.isNotEmpty) {
173 if (fiatConversionStore.prices[trade.to] == null) return '';
174
175 amount = calculateFiatAmount(
176 price: fiatConversionStore.prices[trade.to]!,
177 cryptoAmount: receiveAmount,
178 );
179 }
180 } catch (_) {
181 printV('Error calculating receive amount fiat formatted: $_');
182 }
183 return isFiatDisabled ? '' : '$amount ${fiat.title}';
184 }
185
186 @computed
187 String get sendAmountFiatFormatted {
188 var amount = '0.00';
189 try {
190 if (trade.amount.isNotEmpty) {
191 if (fiatConversionStore.prices[trade.from] == null) return '';
192
193 amount = calculateFiatAmount(
194 price: fiatConversionStore.prices[trade.from]!,
195 cryptoAmount: trade.amount,
196 );
197 }
198 } catch (_) {
199 printV('Error calculating send amount fiat formatted: $_');
200 }
201 return isFiatDisabled ? '' : '$amount ${fiat.title}';
202 }
203
204 void setUpOutput() {
205 sendViewModel.clearOutputs();
206 output = sendViewModel.outputs.first;
207 output.address = trade.inputAddress ?? '';
208 output.setCryptoAmount(trade.amount);
209 if (_provider is ThorChainExchangeProvider) output.memo = trade.memo ?? "";
210 if (trade.isSendAll == true) output.sendAll = true;
211 }
212
213 @action
214 Future<void> confirmSending() async {
215 final canSendError = checkIfCanSend(trade, wallet);
216
217 if (canSendError != null) {
218 _logCanSendError(trade, wallet, canSendError);
219 sendViewModel.state = FailureState(canSendError);
220 return;
221 }
222
223 final selected = trade.from;
224 if (selected == null) {
225 printV('No selectable currency for trade ${trade.id}');
226 return;
227 }
228
229 sendViewModel.selectedCryptoCurrency = selected;
230
231 final pendingTransaction =
232 await sendViewModel.createTransaction(provider: _provider, trade: trade);
233
234 if (_provider is SwapsXyzExchangeProvider) {
235 final hash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? '';
236 trade.txId = hash;
237 await trade.save();
238 }
239
240 if (_provider is ThorChainExchangeProvider) {
241 trade.id = pendingTransaction?.id ?? '';
242 await trade.save();
243 }
244 }
245
246 @action
247 Future<void> _updateTrade() async {
248 try {
249 final updatedTrade = await _provider!.findTradeById(id: trade.id);
250
251 trade.mergeFindTradeByIdResult(updatedTrade);
252 await trade.save();
253 tradesStore.setTrade(trade);
254
255 _updateItems();
256 } catch (e) {
257 printV(e.toString());
258 }
259 }
260
261 void _updateItems() {
262 final tradeFrom = trade.from;
263 final tradeTo = trade.to;
264
265 final tagFrom = tradeFrom?.tag != null ? "${tradeFrom!.tag} " : "";
266 final tagTo = tradeTo?.tag != null ? "${tradeTo!.tag} " : "";
267
268 items.clear();
269
270 if (trade.provider != ExchangeProviderDescription.thorChain)
271 items.add(
272 ExchangeTradeItem(
273 title: "${trade.provider.title} ${S.current.id}",
274 data: "${trade.id}",
275 isCopied: true,
276 isReceiveDetail: true,
277 isExternalSendDetail: false,
278 ),
279 );
280
281 if (tradeFrom != null && tradeTo != null) {
282 items.addAll([
283 ExchangeTradeItem(
284 title: S.current.amount,
285 data:
286 "${_amountParsingProxy.getDisplayCryptoAmount(trade.amount, tradeFrom)} ${_amountParsingProxy.getCryptoSymbol(tradeFrom)}",
287 isCopied: false,
288 isReceiveDetail: false,
289 isExternalSendDetail: true,
290 ),
291 ExchangeTradeItem(
292 title: "${S.current.you_will_receive_estimated_amount}:",
293 data:
294 "${_amountParsingProxy.getDisplayCryptoAmount(trade.receiveAmount ?? "0", tradeTo)} ${_amountParsingProxy.getCryptoSymbol(tradeTo)}",
295 isCopied: true,
296 isReceiveDetail: true,
297 isExternalSendDetail: false,
298 ),
299 ExchangeTradeItem(
300 title: "${S.current.send_to_this_address("$tradeFrom", tagFrom)}:",
301 data: trade.inputAddress ?? '',
302 isCopied: false,
303 isReceiveDetail: false,
304 isExternalSendDetail: true,
305 ),
306 ]);
307
308 items.add(
309 isSwapsXYZCanSendFromExternal
310 ? ExchangeTradeItem(
311 title: S.current.send_to_this_address('${tradeFrom}', tagFrom) + ':',
312 data: trade.inputAddress ?? '',
313 isCopied: false,
314 isReceiveDetail: false,
315 isExternalSendDetail: true)
316 : ExchangeTradeItem(
317 title: 'Smart contract call (no address required)',
318 data: 'Wallet will execute a contract call. On-chain transaction',
319 isCopied: false,
320 isReceiveDetail: false,
321 isExternalSendDetail: true),
322 );
323 }
324
325 final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty;
326
327 if (isExtraIdExist) {
328 final title = tradeFrom == CryptoCurrency.xrp
329 ? S.current.destination_tag
330 : [CryptoCurrency.xlm, CryptoCurrency.ton].contains(tradeFrom)
331 ? S.current.memo
332 : S.current.extra_id;
333
334 items.add(
335 ExchangeTradeItem(
336 title: title,
337 data: trade.extraId ?? "",
338 isCopied: true,
339 isReceiveDetail: !isExtraIdExist,
340 isExternalSendDetail: isExtraIdExist,
341 ),
342 );
343 }
344
345 items.add(
346 ExchangeTradeItem(
347 title: "${S.current.arrive_in_this_address("${tradeTo}", tagTo)}:",
348 data: trade.payoutAddress ?? "",
349 isCopied: true,
350 isReceiveDetail: true,
351 isExternalSendDetail: false,
352 ),
353 );
354 }
355
356 String? checkIfCanSend(Trade? trade, WalletBase wallet) {
357 if (trade == null) return 'Trade is null';
358
359 final tradeFrom = trade.from;
360 if (tradeFrom == null) return 'Trade from currency is null';
361
362 bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b);
363
364 bool _isTokenBelongingToWallet(CryptoCurrency cur) {
365 final chainTag = cur.tag ?? cur.title;
366 return wallet.currency == cur &&
367 (tradeFrom.tag?.toUpperCase() == chainTag.toUpperCase() ||
368 tradeFrom.title.toUpperCase() == chainTag.toUpperCase());
369 }
370
371 final canSend = _sameCurrency(tradeFrom, wallet.currency) ||
372 (_sameCurrency(tradeFrom, CryptoCurrency.btcln) && wallet.currency == CryptoCurrency.btc) ||
373 trade.provider == ExchangeProviderDescription.xmrto ||
374 _isTokenBelongingToWallet(CryptoCurrency.eth) ||
375 _isTokenBelongingToWallet(CryptoCurrency.maticpoly) ||
376 _isTokenBelongingToWallet(CryptoCurrency.baseEth) ||
377 _isTokenBelongingToWallet(CryptoCurrency.arbEth) ||
378 _isTokenBelongingToWallet(CryptoCurrency.trx) ||
379 _isTokenBelongingToWallet(CryptoCurrency.sol) ||
380 _isTokenBelongingToWallet(CryptoCurrency.bnb);
381
382 if (!canSend) {
383 return 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.';
384 }
385
386 return null;
387 }
388
389 void _logCanSendError(Trade? trade, WalletBase wallet, String error) {
390 ExchangeProviderLogger.logError(
391 provider: trade?.provider,
392 function: '_checkIfCanSend',
393 error: error,
394 requestData: {
395 'tradeId': trade?.id,
396 'tradeFrom': trade?.from?.title,
397 'tradeFromTag': trade?.from?.tag,
398 'tradeTo': trade?.to?.title,
399 'tradeToTag': trade?.to?.tag,
400 'walletName': wallet.name,
401 'walletCurrency': wallet.currency.title,
402 'walletCurrencyTag': wallet.currency.tag,
403 },
404 );
405 }
406
407 static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) {
408 final provider = trade.provider;
409
410 if (provider == ExchangeProviderDescription.swapsXyz && isEVMCompatibleChain(wallet.type)) {
411 if (trade.routerData != null && trade.routerData != '0x') {
412 return false;
413 }
414 }
415 return true;
416 }
417
418 PaymentURI? get paymentUri {
419 final inputAddress = trade.inputAddress;
420 final amount = trade.amount;
421 final fromCurrency = trade.from;
422
423 if (inputAddress == null || inputAddress.isEmpty || fromCurrency == null) {
424 return null;
425 }
426
427 // Using the trade's `from` currency so the external-send QR encodes the correct scheme.
428 final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency);
429
430 // for other currencies that we don't have wallets for
431 if (uriWalletType == null) {
432 return ExternalAddressURI(address: inputAddress, amount: amount);
433 }
434
435 printV(uriWalletType);
436
437 switch (uriWalletType) {
438 case WalletType.bitcoin:
439 return BitcoinURI(address: inputAddress, amount: amount);
440 case WalletType.bitcoinCash:
441 return BitcoinCashURI(address: inputAddress, amount: amount);
442 case WalletType.dogecoin:
443 return DogeURI(address: inputAddress, amount: amount);
444 case WalletType.ethereum:
445 case WalletType.polygon:
446 case WalletType.base:
447 case WalletType.arbitrum:
448 case WalletType.bsc:
449 return _createERC681URI(fromCurrency, inputAddress, amount);
450 case WalletType.solana:
451 return SolanaURI(
452 amount: amount,
453 address: inputAddress,
454 contractAddress: TokenUtilities.findSolanaTokenMint(fromCurrency),
455 );
456 case WalletType.tron:
457 return TronURI(
458 amount: amount,
459 address: inputAddress,
460 contractAddress: TokenUtilities.findTronTokenContract(fromCurrency),
461 );
462 case WalletType.monero:
463 return MoneroURI(address: inputAddress, amount: amount);
464 case WalletType.wownero:
465 return MoneroURI(address: inputAddress, amount: amount);
466 case WalletType.litecoin:
467 return LitecoinURI(amount: amount, address: inputAddress);
468 case WalletType.nano:
469 return NanoURI(amount: amount, address: inputAddress);
470 case WalletType.zano:
471 return ZanoURI(amount: amount, address: inputAddress);
472 case WalletType.decred:
473 return DecredURI(amount: amount, address: inputAddress);
474 case WalletType.zcash:
475 return ZcashURI(amount: amount, address: inputAddress);
476 case WalletType.banano:
477 case WalletType.none:
478 case WalletType.haven:
479 return null;
480 }
481 }
482
483 @action
484 PaymentURI? _createERC681URI(CryptoCurrency currency, String address, String amount) {
485 final chainId = TokenUtilities.getChainId(currency);
486 final isNativeToken = TokenUtilities.isNativeToken(currency);
487
488 if (isNativeToken) {
489 return ERC681URI(
490 chainId: chainId,
491 address: address,
492 amount: amount,
493 contractAddress: null,
494 );
495 } else {
496 final erc20Token = TokenUtilities.findErc20Token(currency, wallet) ??
497 TokenUtilities.findErc20TokenForSwap(currency);
498
499 if (erc20Token != null) {
500 return ERC681URI(
501 chainId: chainId,
502 address: address,
503 amount: amount,
504 contractAddress: erc20Token.contractAddress,
505 tokenDecimals: erc20Token.decimal,
506 );
507 }
508 return null;
509 }
510 }
511
512 @computed
513 String get qrImage => getQrImage(wallet.type);
514 }