dev
dart 1,592 lines 51.6 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io' show Platform;
4
5 import 'package:cake_wallet/.secrets.g.dart' as secrets;
6 import 'package:cake_wallet/bitcoin/bitcoin.dart';
7 import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
8 import 'package:cake_wallet/core/key_service.dart';
9 import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
10 import "package:cw_core/balance_card_style_settings.dart";
11 import 'package:cake_wallet/core/trade_monitor.dart';
12 import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
13 import 'package:cake_wallet/entities/balance_display_mode.dart';
14 import 'package:cake_wallet/entities/exchange_api_mode.dart';
15 import 'package:cake_wallet/entities/preferences_key.dart';
16 import 'package:cake_wallet/entities/service_status.dart';
17 import 'package:cake_wallet/entities/sync_status_display_mode.dart';
18 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
19 import 'package:cake_wallet/generated/i18n.dart';
20 import 'package:cake_wallet/monero/monero.dart';
21 import 'package:cake_wallet/nano/nano.dart';
22 import 'package:cake_wallet/order/order_provider_description.dart';
23 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
24 import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
25 import 'package:cake_wallet/utils/device_info.dart';
26 import 'package:cake_wallet/utils/show_pop_up.dart';
27 import 'package:cake_wallet/zcash/zcash.dart';
28 import 'package:cw_core/transaction_direction.dart';
29 import 'package:cw_core/utils/proxy_wrapper.dart';
30 import 'package:cake_wallet/utils/tor.dart';
31 import 'package:cake_wallet/wownero/wownero.dart' as wow;
32 import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
33 import 'package:cake_wallet/store/app_store.dart';
34 import 'package:cake_wallet/store/dashboard/orders_store.dart';
35 import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
36 import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
37 import 'package:cake_wallet/store/dashboard/trades_store.dart';
38 import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
39 import 'package:cake_wallet/store/settings_store.dart';
40 import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
41 import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
42 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
43 import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
44 import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart';
45 import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
46 import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
47 import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
48 import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
49 import 'package:cake_wallet/view_model/settings/sync_mode.dart';
50 import 'package:cryptography/cryptography.dart';
51 import 'package:cw_core/balance.dart';
52 import 'package:cw_core/card_design.dart';
53 import 'package:cw_core/crypto_currency.dart';
54 import 'package:cw_core/pathForWallet.dart';
55 import 'package:cw_core/sync_status.dart';
56 import 'package:cw_core/transaction_history.dart';
57 import 'package:cw_core/transaction_info.dart';
58 import 'package:cw_core/utils/file.dart';
59 import 'package:cw_core/utils/print_verbose.dart';
60 import 'package:cw_core/wallet_base.dart';
61 import 'package:cw_core/wallet_info.dart';
62 import 'package:cw_core/wallet_type.dart';
63 import 'package:eth_sig_util/util/utils.dart';
64 import 'package:flutter/material.dart';
65 import 'package:flutter/services.dart';
66 import 'package:flutter_daemon/flutter_daemon.dart';
67 import 'package:mobx/mobx.dart';
68 import 'package:permission_handler/permission_handler.dart';
69 import 'package:shared_preferences/shared_preferences.dart';
70
71 import 'package:cake_wallet/reactions/wallet_connect.dart';
72 import 'package:cake_wallet/evm/evm.dart';
73
74 part 'dashboard_view_model.g.dart';
75
76 class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
77
78 abstract class DashboardViewModelBase with Store {
79 DashboardViewModelBase(
80 {required this.balanceViewModel,
81 required this.tradeMonitor,
82 required this.appStore,
83 required this.tradesStore,
84 required this.tradeFilterStore,
85 required this.orderFilterStore,
86 required this.transactionFilterStore,
87 required this.settingsStore,
88 required this.yatStore,
89 required this.ordersStore,
90 required this.anonpayTransactionsStore,
91 required this.payjoinTransactionsStore,
92 required this.sharedPreferences,
93 required this.keyService})
94 : hasTradeAction = true,
95 hasSwapAction = true,
96 isShowFirstYatIntroduction = false,
97 isShowSecondYatIntroduction = false,
98 isShowThirdYatIntroduction = false,
99 isMigratingToIronwood = false,
100 filterItems = [],
101 exchangeFilterItems = [],
102 subname = '',
103 name = appStore.wallet!.name,
104 type = appStore.wallet!.type,
105 transactions = ObservableList<TransactionListItem>(),
106 cardDesigns = ObservableList<CardDesign>(),
107 cardOrder = ObservableMap<int, int>(),
108 wallet = appStore.wallet! {
109 showDecredInfoCard = wallet.type == WalletType.decred &&
110 (sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true);
111
112 name = wallet.name;
113 type = wallet.type;
114 isShowFirstYatIntroduction = false;
115 isShowSecondYatIntroduction = false;
116 isShowThirdYatIntroduction = false;
117 unawaited(isBackgroundSyncEnabled());
118 unawaited(isBatteryOptimizationEnabled());
119 unawaited(_loadConstraints());
120 final _wallet = wallet;
121
122 loadFilterItems();
123
124 if (_wallet.type == WalletType.monero) {
125 subname = monero!.getCurrentAccount(_wallet).label;
126
127 _onMoneroAccountChangeReaction = reaction(
128 (_) => monero!.getMoneroWalletDetails(wallet).account,
129 (Account account) => _onMoneroAccountChange(_wallet));
130
131 _onMoneroBalanceChangeReaction = reaction(
132 (_) => monero!.getMoneroWalletDetails(wallet).balance,
133 (MoneroBalance balance) => _onMoneroTransactionsUpdate(_wallet));
134
135 final _accountTransactions = _wallet.transactionHistory.transactions.values
136 .where((tx) =>
137 monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id)
138 .toList();
139
140 final sortedTransactions = [..._accountTransactions];
141 sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
142
143 transactions = ObservableList.of(
144 sortedTransactions.map(
145 (transaction) => TransactionListItem(
146 transaction: transaction,
147 balanceViewModel: balanceViewModel,
148 appStore: appStore,
149 key: ValueKey('monero_transaction_history_item_${transaction.id}_key'),
150 ),
151 ),
152 );
153 } else if (_wallet.type == WalletType.wownero) {
154 subname = wow.wownero!.getCurrentAccount(_wallet).label;
155
156 _onMoneroAccountChangeReaction = reaction(
157 (_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
158 (wow.Account account) => _onMoneroAccountChange(_wallet));
159
160 _onMoneroBalanceChangeReaction = reaction(
161 (_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
162 (wow.WowneroBalance balance) => _onMoneroTransactionsUpdate(_wallet));
163
164 final _accountTransactions = _wallet.transactionHistory.transactions.values
165 .where((tx) =>
166 wow.wownero!.getTransactionInfoAccountId(tx) ==
167 wow.wownero!.getCurrentAccount(wallet).id)
168 .toList();
169
170 final sortedTransactions = [..._accountTransactions];
171 sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
172
173 transactions = ObservableList.of(
174 sortedTransactions.map(
175 (transaction) => TransactionListItem(
176 transaction: transaction,
177 balanceViewModel: balanceViewModel,
178 appStore: appStore,
179 key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'),
180 ),
181 ),
182 );
183 } else {
184 final sortedTransactions = [...wallet.transactionHistory.transactions.values];
185 sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
186
187 transactions = ObservableList.of(
188 sortedTransactions.map(
189 (transaction) => TransactionListItem(
190 transaction: transaction,
191 balanceViewModel: balanceViewModel,
192 appStore: appStore,
193 key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'),
194 ),
195 ),
196 );
197 }
198
199 // TODO: nano sub-account generation is disabled:
200 // if (_wallet.type == WalletType.nano || _wallet.type == WalletType.banano) {
201 // subname = nano!.getCurrentAccount(_wallet).label;
202 // }
203
204 _walletChangeDisposer?.reaction.dispose();
205 _walletChangeDisposer = reaction((_) => appStore.wallet, (wallet) {
206 _onWalletChange(wallet);
207 _checkMweb();
208 loadCardDesigns();
209 showDecredInfoCard = wallet?.type == WalletType.decred &&
210 sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) != false;
211
212 tradeMonitor.stopTradeMonitoring();
213 tradeMonitor.monitorActiveTrades(wallet!.id);
214 });
215
216 _transactionDisposer?.reaction.dispose();
217 _transactionDisposer = reaction((_) {
218 final length = appStore.wallet!.transactionHistory.transactions.length;
219 if (length == 0) {
220 return 0;
221 }
222 int confirmations = 1;
223 if (![WalletType.solana, WalletType.tron].contains(wallet.type)) {
224 try {
225 confirmations =
226 appStore.wallet!.transactionHistory.transactions.values.first.confirmations +
227 appStore.wallet!.transactionHistory.transactions.values.last.confirmations +
228 1;
229 } catch (_) {}
230 } else {
231 final pendingCount = appStore.wallet!.transactionHistory.transactions.values
232 .where((item) => item.isPending)
233 .length;
234 confirmations = pendingCount + 1;
235 }
236 return length * confirmations;
237 }, _transactionDisposerCallback, delay: 300);
238
239 if (hasSilentPayments) {
240 silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
241
242 reaction((_) => wallet.syncStatus, (SyncStatus syncStatus) {
243 silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
244 });
245 }
246
247 loadCardDesigns();
248
249 _checkMweb();
250 reaction((_) => settingsStore.mwebAlwaysScan, (bool value) => _checkMweb());
251
252 reaction((_) => tradesStore.trades, (_) => tradeMonitor.monitorActiveTrades(wallet.id));
253
254 addZcashMigrationReaction();
255
256 tradeMonitor.monitorActiveTrades(wallet.id);
257 }
258
259 void loadFilterItems() {
260 filterItems = [
261 // FilterItem(
262 // value: () => transactionFilterStore.displayAll,
263 // caption: S.current.all_transactions,
264 // onChanged: transactionFilterStore.toggleAll),
265 FilterItem(
266 value: () => transactionFilterStore.displayOutgoing,
267 caption: S.current.send,
268 onChanged: transactionFilterStore.toggleOutgoing),
269 FilterItem(
270 value: () => transactionFilterStore.displayIncoming,
271 caption: S.current.receive,
272 onChanged: transactionFilterStore.toggleIncoming),
273 if (appStore.wallet!.type == WalletType.bitcoin)
274 FilterItem(
275 value: () => transactionFilterStore.displaySilentPayments,
276 caption: S.current.silent_payments,
277 onChanged: transactionFilterStore.toggleSilentPayments,
278 ),
279 SwapFilterItem(
280 enabledProviders: () => tradeFilterStore.enabledProvidersCount,
281 allEnabled: () => tradeFilterStore.displayAllTrades,
282 value: () => tradeFilterStore.enabledProvidersCount > 0,
283 onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
284 FilterItem(
285 value: () => orderFilterStore.displayCakePay,
286 caption: 'Cake Pay',
287 onChanged: () => orderFilterStore.toggleDisplayOrder(OrderProviderDescription.cakePay)),
288 ];
289 exchangeFilterItems = [
290 SwapProviderFilterItem(
291 providerDescription: ExchangeProviderDescription.changeNow,
292 value: () => tradeFilterStore.displayChangeNow,
293 onChanged: () =>
294 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.changeNow)),
295 SwapProviderFilterItem(
296 providerDescription: ExchangeProviderDescription.sideShift,
297 value: () => tradeFilterStore.displaySideShift,
298 onChanged: () =>
299 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.sideShift)),
300 SwapProviderFilterItem(
301 providerDescription: ExchangeProviderDescription.simpleSwap,
302 value: () => tradeFilterStore.displaySimpleSwap,
303 onChanged: () =>
304 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)),
305 SwapProviderFilterItem(
306 providerDescription: ExchangeProviderDescription.trocador,
307 value: () => tradeFilterStore.displayTrocador,
308 onChanged: () =>
309 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
310 SwapProviderFilterItem(
311 providerDescription: ExchangeProviderDescription.exolix,
312 value: () => tradeFilterStore.displayExolix,
313 onChanged: () =>
314 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
315 SwapProviderFilterItem(
316 providerDescription: ExchangeProviderDescription.chainflip,
317 value: () => tradeFilterStore.displayChainflip,
318 onChanged: () =>
319 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)),
320 SwapProviderFilterItem(
321 providerDescription: ExchangeProviderDescription.thorChain,
322 value: () => tradeFilterStore.displayThorChain,
323 onChanged: () =>
324 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)),
325 SwapProviderFilterItem(
326 providerDescription: ExchangeProviderDescription.letsExchange,
327 value: () => tradeFilterStore.displayLetsExchange,
328 onChanged: () =>
329 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)),
330 SwapProviderFilterItem(
331 providerDescription: ExchangeProviderDescription.stealthEx,
332 value: () => tradeFilterStore.displayStealthEx,
333 onChanged: () =>
334 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)),
335 SwapProviderFilterItem(
336 providerDescription: ExchangeProviderDescription.xoSwap,
337 value: () => tradeFilterStore.displayXOSwap,
338 onChanged: () =>
339 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)),
340 SwapProviderFilterItem(
341 providerDescription: ExchangeProviderDescription.swapTrade,
342 value: () => tradeFilterStore.displaySwapTrade,
343 onChanged: () =>
344 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapTrade)),
345 SwapProviderFilterItem(
346 providerDescription: ExchangeProviderDescription.swapsXyz,
347 value: () => tradeFilterStore.displaySwapXyz,
348 onChanged: () =>
349 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)),
350 SwapProviderFilterItem(
351 providerDescription: ExchangeProviderDescription.nearIntents,
352 value: () => tradeFilterStore.displayNearIntents,
353 onChanged: () =>
354 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.nearIntents)),
355 ];
356 }
357
358 bool _isTransactionDisposerCallbackRunning = false;
359
360 @action
361 void _reloadTransactions() {
362 if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero) {
363 return; // Monero/Wownero transactions are handled separately
364 }
365
366 transactions.clear();
367
368 transactions.addAll(
369 wallet.transactionHistory.transactions.values.map(
370 (transaction) => TransactionListItem(
371 transaction: transaction,
372 balanceViewModel: balanceViewModel,
373 appStore: appStore,
374 key: ValueKey('${wallet.type.name}_transaction_history_item_${transaction.id}_key'),
375 ),
376 ),
377 );
378 }
379
380 @observable
381 ReactionDisposer? zcashMigrationReactionDisposer;
382
383 @action
384 void addZcashMigrationReaction() {
385 zcashMigrationReactionDisposer?.reaction.dispose();
386 zcashMigrationReactionDisposer = null;
387
388 if (wallet.type == WalletType.zcash) {
389 zcashMigrationReactionDisposer = reaction((_) => wallet.balance.values.first, (_) async {
390 isMigratingToIronwood =
391 wallet.type == WalletType.zcash && await zcash!.hasOrchardMigratableBalance(wallet);
392 });
393 }
394 }
395
396 @computed
397 bool get isSyncHeavy {
398 if ([
399 WalletType.monero,
400 WalletType.wownero,
401 WalletType.decred,
402 WalletType.zcash,
403 WalletType.zano
404 ].contains(wallet.type)) {
405 return true;
406 }
407
408 if (wallet.type == WalletType.bitcoin && silentPaymentsScanningActive && hasSilentPayments) {
409 return true;
410 }
411
412 if (wallet.type == WalletType.litecoin && mwebEnabled && hasMweb) {
413 return true;
414 }
415
416 return false;
417 }
418
419 bool showBridge(CryptoCurrency currency) {
420 if (!isEVMCompatibleChain(wallet.type)) return false;
421
422 if (evm!.isUSDT0Token(wallet, currency)) return true;
423
424 return false;
425 }
426
427 @action
428 void changeAllFilterItems(bool value) {
429 for (final item in filterItems) {
430 if (item.value() != value) {
431 item.onChanged();
432 }
433 }
434 for (final item in exchangeFilterItems) {
435 if (item.value() != value) {
436 item.onChanged();
437 }
438 }
439 }
440
441 @action
442 Future<void> loadCardDesigns() async {
443 final accountStyleSettings =
444 await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
445
446 late final int numAccounts;
447 if (wallet.type == WalletType.monero) {
448 numAccounts = monero!.getAccountList(wallet).accounts.length;
449 } else if (wallet.type == WalletType.wownero) {
450 numAccounts = wow.wownero!.getAccountList(wallet).accounts.length;
451 } else if (wallet.type == WalletType.bitcoin) {
452 // bitcoin and lightning
453 numAccounts = 2;
454 } else {
455 numAccounts = 1;
456 }
457 cardDesigns.clear();
458 Map<int, int> newOrder = {};
459
460 for (int i = 0; i < numAccounts; i++) {
461 late final int index;
462 if (balanceViewModel.hasAccounts) {
463 index = i;
464 } else if (wallet.type == WalletType.bitcoin && i == 1) {
465 index = 0;
466 } else {
467 index = -1;
468 }
469
470 final setting = accountStyleSettings.where((e) => e.accountIndex == index).firstOrNull;
471
472 late final CryptoCurrency curr;
473 if (wallet.type == WalletType.bitcoin && i == 1) {
474 curr = CryptoCurrency.btcln;
475 } else {
476 curr = wallet.currency;
477 }
478
479 cardDesigns.add(CardDesign.fromStyleSettings(setting, curr));
480 if (setting?.cardOrder != null) {
481 newOrder[setting!.cardOrder] = i;
482 }
483 }
484
485 // making sure ALL accounts have numbers, even the ones that existed before this feature was a thing
486 for (int i = 0; i < numAccounts; i++) {
487 if (!newOrder.containsKey(i) && !(wallet.type != WalletType.bitcoin && i == 1)) {
488 int free = 0;
489 while (newOrder.containsValue(free)) {
490 free++;
491 }
492 if (wallet.type == WalletType.bitcoin) {
493 newOrder[free] = 0;
494 } else {
495 newOrder[free] = i;
496 }
497 }
498 }
499 cardOrder = newOrder.asObservable();
500 }
501
502 void _transactionDisposerCallback(int _) async {
503 // Simple check to prevent the callback from being called multiple times in the same frame
504 if (_isTransactionDisposerCallbackRunning) return;
505 _isTransactionDisposerCallbackRunning = true;
506 await Future.delayed(Duration.zero);
507
508 try {
509 final currentAccountId = wallet.type == WalletType.monero
510 ? monero!.getCurrentAccount(wallet).id
511 : wallet.type == WalletType.wownero
512 ? wow.wownero!.getCurrentAccount(wallet).id
513 : null;
514 final List<TransactionInfo> relevantTxs = [];
515
516 for (final tx in appStore.wallet!.transactionHistory.transactions.values) {
517 bool isRelevant = true;
518 if (wallet.type == WalletType.monero) {
519 isRelevant = monero!.getTransactionInfoAccountId(tx) == currentAccountId;
520 } else if (wallet.type == WalletType.wownero) {
521 isRelevant = wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId;
522 }
523
524 if (isRelevant) {
525 relevantTxs.add(tx);
526 }
527 }
528 // printV("Transaction disposer callback (relevantTxs: ${relevantTxs.length} current: ${transactions.length})");
529
530 // TODO(malik) update this in a saner way during the vm refactor
531 String _txIdentityString(String txHash, TransactionDirection direction) =>
532 "${txHash}_$direction";
533 String _txIdentityStringConfirmations(
534 String txHash, TransactionDirection direction, int confirmations, bool isPending) =>
535 "${txHash}_${direction}_${confirmations}_$isPending";
536
537 final existingKeys = transactions
538 .map((item) => _txIdentityStringConfirmations(
539 item.transaction.txHash,
540 item.transaction.direction,
541 item.transaction.confirmations,
542 item.transaction.isPending))
543 .toSet();
544
545 final newTransactions = relevantTxs
546 .where((tx) => !existingKeys.contains(_txIdentityStringConfirmations(
547 tx.txHash, tx.direction, tx.confirmations, tx.isPending)))
548 .map((tx) => TransactionListItem(
549 transaction: tx,
550 balanceViewModel: balanceViewModel,
551 appStore: appStore,
552 key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
553 ))
554 .toList();
555
556 final newKeys = newTransactions
557 .map((item) => _txIdentityString(item.transaction.txHash, item.transaction.direction))
558 .toSet();
559
560 transactions.removeWhere((item) {
561 if (wallet.type == WalletType.zcash) {
562 return newTransactions.any(
563 (n) => n.transaction.txHash == item.transaction.txHash,
564 );
565 }
566 return newKeys.contains(
567 _txIdentityString(item.transaction.txHash, item.transaction.direction),
568 );
569 });
570
571 transactions.addAll(newTransactions);
572 // transactions.clear();
573 // transactions.addAll(relevantTxs.map((tx) => TransactionListItem(
574 // transaction: tx,
575 // balanceViewModel: balanceViewModel,
576 // appStore: appStore,
577 // key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
578 // )));
579 } finally {
580 _isTransactionDisposerCallbackRunning = false;
581 }
582 }
583
584 void _checkMweb() {
585 if (hasMweb) {
586 mwebEnabled = bitcoin!.getMwebEnabled(wallet);
587 balanceViewModel.mwebEnabled = mwebEnabled;
588 }
589 }
590
591 @observable
592 WalletType type;
593
594 @observable
595 String name;
596
597 @observable
598 ObservableList<TransactionListItem> transactions;
599
600 @observable
601 String subname;
602
603 @observable
604 bool isShowFirstYatIntroduction;
605
606 @observable
607 bool isShowSecondYatIntroduction;
608
609 @observable
610 bool isShowThirdYatIntroduction;
611
612 @observable
613 ObservableList<CardDesign> cardDesigns;
614
615 @observable
616 ObservableMap<int, int> cardOrder;
617
618 @computed
619 bool get isDarkTheme => appStore.themeStore.currentTheme.isDark;
620
621 @computed
622 String get address => wallet.walletAddresses.address;
623
624 @computed
625 bool get isTorEnabled => settingsStore.currentBuiltinTor;
626
627 @computed
628 SyncStatus get status => wallet.syncStatus;
629
630 @computed
631 bool get shouldShowMwebAd {
632 if (wallet.type != WalletType.litecoin) return false;
633
634 if (mwebEnabled) return false;
635
636 if (settingsStore.mwebAdDismissed) return false;
637
638 return (Platform.isAndroid || Platform.isIOS) && !wallet.isHardwareWallet;
639 }
640
641 @action
642 void dismissMwebAd(bool enableMweb) {
643 if (enableMweb) setMwebEnabled();
644
645 settingsStore.mwebAdDismissed = true;
646 }
647
648 @computed
649 String get syncStatusText {
650 var statusText = '';
651
652 if (status is SyncingSyncStatus) {
653 statusText = S.current.Blocks_remaining(status.toString());
654 }
655
656 if (status is FailedSyncStatus || status is LostConnectionSyncStatus) {
657 statusText = S.current.please_try_to_connect_to_another_node;
658 }
659
660 if (status is ProcessingSyncStatus) {
661 statusText = (status as ProcessingSyncStatus).message ?? S.current.processing;
662 }
663
664 return statusText;
665 }
666
667 @computed
668 double get confirmationProgress {
669 int received = 0;
670 int needed = 0;
671
672 for (final transaction in transactions) {
673 if (transaction.neededConfirmations == 0) {
674 continue;
675 }
676
677 if (transaction.transaction.confirmations >= transaction.neededConfirmations) {
678 continue;
679 }
680
681 received += transaction.transaction.confirmations;
682 needed += transaction.neededConfirmations;
683 }
684 if (needed == 0) {
685 return 1;
686 }
687 return received / needed;
688 }
689
690 @computed
691 BalanceDisplayMode get balanceDisplayMode => appStore.settingsStore.balanceDisplayMode;
692
693 @computed
694 @Deprecated("Replaced by showApps")
695 bool get shouldShowMarketPlaceInDashboard =>
696 appStore.settingsStore.shouldShowMarketPlaceInDashboard;
697
698 @computed
699 bool get showApps => appStore.settingsStore.shouldShowMarketPlaceInDashboard;
700
701 @computed
702 List<TradeListItem> get trades => tradesStore.trades.where((trade) {
703 final isSameChain = trade.trade.chainId != null
704 ? trade.trade.chainId == wallet.chainId
705 : true; // returning default as true here so it falls back to the default checks if there's no chainId
706 return trade.trade.walletId == wallet.id && isSameChain;
707 }).toList();
708
709 @computed
710 bool get shouldShowBalanceHiddenMessage =>
711 balanceDisplayMode == BalanceDisplayMode.hiddenBalance &&
712 appStore.settingsStore.balanceHideCounter < 10;
713
714 @computed
715 List<OrderListItem> get orders =>
716 ordersStore.orders.where((item) => item.order.walletId == wallet.id).toList();
717
718 @computed
719 List<AnonpayTransactionListItem> get anonpayTransactions => anonpayTransactionsStore.transactions
720 .where((item) => item.transaction.walletId == wallet.id)
721 .toList();
722
723 @computed
724 List<PayjoinTransactionListItem> get payjoinTransactions => payjoinTransactionsStore.transactions
725 .where((item) => item.session.walletId == wallet.id)
726 .toList();
727
728 @computed
729 double get price => balanceViewModel.price;
730
731 @computed
732 bool get isAutoGenerateSubaddressesEnabled =>
733 settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled;
734
735 @computed
736 List<ActionListItem> get items {
737 final _items = <ActionListItem>[];
738
739 _items.addAll(
740 transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactions]));
741 _items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
742 _items.addAll(orderFilterStore.filtered(orders: orders, wallet: wallet));
743
744 if (payjoinTransactions.isNotEmpty) {
745 final _payjoinTransactions = payjoinTransactions;
746 _items.forEach((e) {
747 if (e is TransactionListItem &&
748 _payjoinTransactions.any((t) => t.session.txId == e.transaction.id)) {
749 _payjoinTransactions.firstWhere((t) => t.session.txId == e.transaction.id).transaction =
750 e.transaction;
751 }
752 });
753 _items.addAll(_payjoinTransactions);
754 _items.removeWhere((e) => (e is TransactionListItem &&
755 _payjoinTransactions.any((t) => t.session.txId == e.transaction.id)));
756 }
757
758 return formattedItemsList(_items);
759 }
760
761 static const shortHistoryLength = 3;
762
763 @computed
764 List<ActionListItem> get itemsShort =>
765 items.where((item) => item is! DateSectionItem).take(shortHistoryLength).toList();
766
767 @observable
768 WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
769
770 @computed
771 bool get hasLightning =>
772 wallet.type == WalletType.bitcoin && wallet.isSoftwareWallet && bitcoin!.useLightning(wallet);
773
774 @computed
775 bool get hasWalletConnect =>
776 isWalletConnectCompatibleChain(wallet.type) && !wallet.isHardwareWallet;
777
778 @computed
779 bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet);
780
781 @computed
782 bool get hasRescan => wallet.hasRescan;
783
784 @computed
785 bool get hasBackgroundSync => [
786 WalletType.monero,
787 ].contains(wallet.type);
788
789 @computed
790 bool get isMoneroViewOnly {
791 if (wallet.type != WalletType.monero) return false;
792 return monero!.isViewOnly();
793 }
794
795 @computed
796 String? get getMoneroError {
797 if (wallet.type != WalletType.monero) return null;
798 try {
799 monero!.monerocCheck();
800 } catch (e) {
801 return e.toString();
802 }
803 return null;
804 }
805
806 @computed
807 String? get getWowneroError {
808 if (wallet.type != WalletType.wownero) return null;
809 try {
810 wow.wownero!.wownerocCheck();
811 } catch (e) {
812 return e.toString();
813 }
814 return null;
815 }
816
817 List<String> get isMoneroWalletBrokenReasons {
818 if (wallet.type != WalletType.monero) return [];
819 final keys = monero!.getKeys(wallet);
820 List<String> errors = [
821 // leaving these commented out for now, I'll be able to fix that properly in the airgap update
822 // to not cause work duplication, this will do the job as well, it will be slightly less precise
823 // about what happened - but still enough.
824 // if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0",
825 if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("") &&
826 !wallet.isHardwareWallet)
827 "private view key is 0",
828 // if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0",
829 if (keys['publicViewKey'] == List.generate(64, (index) => "0").join(""))
830 "public view key is 0",
831 // if (wallet.seed == null) "wallet seed is null",
832 // if (wallet.seed == "") "wallet seed is empty",
833 // if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address ==
834 // "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4")
835 // "primary address is invalid, you won't be able to receive / spend funds",
836 ];
837 return errors;
838 }
839
840 @computed
841 bool get showZcashMissingFundsCard {
842 if (wallet.type != WalletType.zcash) return false;
843 if (!settingsStore.showZcashMissingFundsCard) return false;
844 return zcash!.showMissingFundsCard(wallet);
845 }
846
847 @computed
848 bool get hasSilentPayments =>
849 wallet.type == WalletType.bitcoin &&
850 (bitcoin!.getWalletKeys(wallet)["privateKey"] ?? "").isNotEmpty &&
851 !wallet.isHardwareWallet;
852
853 @computed
854 bool get showSilentPaymentsCard => hasSilentPayments && settingsStore.silentPaymentsCardDisplay;
855
856 @computed
857 bool get isEVMWallet => isEVMCompatibleChain(wallet.type);
858
859 @computed
860 List<ChainInfo> get availableChains {
861 if (!isEVMWallet) return [];
862 return evm!.getAllChains();
863 }
864
865 @computed
866 ChainInfo? get currentChain {
867 if (!isEVMWallet) return null;
868 return evm!.getCurrentChain(wallet);
869 }
870
871 @action
872 Future<void> selectChain(int chainId) async {
873 if (!isEVMWallet) return;
874
875 final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
876
877 await evm!.selectChain(wallet, chainId, node: node);
878 }
879
880 final KeyService keyService;
881 final SharedPreferences sharedPreferences;
882
883 @observable
884 bool silentPaymentsScanningActive = false;
885
886 @action
887 void setSilentPaymentsScanning(bool active) {
888 silentPaymentsScanningActive = active;
889
890 if (hasSilentPayments) {
891 bitcoin!.setScanningActive(wallet, active);
892 }
893 }
894
895 @computed
896 bool get hasMweb =>
897 wallet.type == WalletType.litecoin &&
898 (Platform.isIOS || Platform.isAndroid) &&
899 !wallet.isHardwareWallet;
900
901 @computed
902 bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled;
903
904 @observable
905 bool mwebEnabled = false;
906
907 @observable
908 late bool showDecredInfoCard;
909
910 @computed
911 bool get showPayjoinCard =>
912 wallet.type == WalletType.bitcoin &&
913 settingsStore.showPayjoinCard &&
914 !settingsStore.usePayjoin &&
915 DeviceInfo.instance.isMobile;
916
917 @observable
918 bool backgroundSyncEnabled = false;
919
920 @action
921 Future<bool> isBackgroundSyncEnabled() async {
922 if (!Platform.isAndroid) {
923 return false;
924 }
925 final resp = await FlutterDaemon().getBackgroundSyncStatus();
926 backgroundSyncEnabled = resp;
927 return resp;
928 }
929
930 @action
931 void toggleSwitchStatusDisplayMode() {
932 if (status is SyncingSyncStatus &&
933 !((status as SyncingSyncStatus).shouldShowBlocksRemaining())) {
934 if (settingsStore.syncStatusDisplayMode == SyncStatusDisplayMode.eta) {
935 settingsStore.syncStatusDisplayMode = SyncStatusDisplayMode.blocksRemaining;
936 } else {
937 settingsStore.syncStatusDisplayMode = SyncStatusDisplayMode.eta;
938 }
939 }
940 }
941
942 @observable
943 late bool backgroundSyncNotificationsEnabled =
944 sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false;
945
946 @action
947 Future<void> setBackgroundSyncNotificationsEnabled(bool value) async {
948 if (!value) {
949 backgroundSyncNotificationsEnabled = false;
950 sharedPreferences.setBool(PreferencesKey.backgroundSyncNotificationsEnabled, false);
951 return;
952 }
953 PermissionStatus permissionStatus = await Permission.notification.status;
954 if (permissionStatus != PermissionStatus.granted) {
955 final resp = await Permission.notification.request();
956 if (resp == PermissionStatus.denied) {
957 throw Exception("Notification permission denied");
958 }
959 }
960 backgroundSyncNotificationsEnabled = value;
961 await sharedPreferences.setBool(PreferencesKey.backgroundSyncNotificationsEnabled, value);
962 }
963
964 bool get hasBgsyncNetworkConstraints => Platform.isAndroid;
965
966 bool get hasBgsyncBatteryNotLowConstraints => Platform.isAndroid;
967
968 bool get hasBgsyncChargingConstraints => Platform.isAndroid;
969
970 bool get hasBgsyncDeviceIdleConstraints => Platform.isAndroid;
971
972 @observable
973 bool backgroundSyncNetworkUnmetered = false;
974
975 @observable
976 bool backgroundSyncBatteryNotLow = false;
977
978 @observable
979 bool backgroundSyncCharging = false;
980
981 @observable
982 bool backgroundSyncDeviceIdle = false;
983
984 Future<void> _loadConstraints() async {
985 if (Platform.isAndroid) {
986 backgroundSyncNetworkUnmetered = await FlutterDaemon().getNetworkType();
987 backgroundSyncBatteryNotLow = await FlutterDaemon().getBatteryNotLow();
988 backgroundSyncCharging = await FlutterDaemon().getRequiresCharging();
989 backgroundSyncDeviceIdle = await FlutterDaemon().getDeviceIdle();
990 }
991 }
992
993 @action
994 Future<void> setBackgroundSyncNetworkUnmetered(bool value) async {
995 backgroundSyncNetworkUnmetered = value;
996 await FlutterDaemon().setNetworkType(value);
997 if (await isBackgroundSyncEnabled()) {
998 await enableBackgroundSync();
999 }
1000 }
1001
1002 @action
1003 Future<void> setBackgroundSyncBatteryNotLow(bool value) async {
1004 backgroundSyncBatteryNotLow = value;
1005 await FlutterDaemon().setBatteryNotLow(value);
1006 if (await isBackgroundSyncEnabled()) {
1007 await enableBackgroundSync();
1008 }
1009 }
1010
1011 @action
1012 Future<void> setBackgroundSyncCharging(bool value) async {
1013 backgroundSyncCharging = value;
1014 await FlutterDaemon().setRequiresCharging(value);
1015 if (await isBackgroundSyncEnabled()) {
1016 await enableBackgroundSync();
1017 }
1018 }
1019
1020 @action
1021 Future<void> setBackgroundSyncDeviceIdle(bool value) async {
1022 backgroundSyncDeviceIdle = value;
1023 await FlutterDaemon().setDeviceIdle(value);
1024 if (await isBackgroundSyncEnabled()) {
1025 await enableBackgroundSync();
1026 }
1027 }
1028
1029 bool get hasBatteryOptimization => Platform.isAndroid;
1030
1031 @observable
1032 bool batteryOptimizationEnabled = false;
1033
1034 @action
1035 Future<bool> isBatteryOptimizationEnabled() async {
1036 if (!hasBatteryOptimization) {
1037 return false;
1038 }
1039 final resp = await FlutterDaemon().isBatteryOptimizationDisabled();
1040 batteryOptimizationEnabled = !resp;
1041 if (batteryOptimizationEnabled && await isBackgroundSyncEnabled()) {
1042 // If the battery optimization is enabled, we need to disable the background sync
1043 await disableBackgroundSync();
1044 }
1045 return resp;
1046 }
1047
1048 @action
1049 Future<void> disableBatteryOptimization() async {
1050 final resp = await FlutterDaemon().requestDisableBatteryOptimization();
1051 unawaited((() async {
1052 // android doesn't return if the permission was granted, so we need to poll it,
1053 // minute should be enough for the fallback method (opening settings and changing the permission)
1054 for (var i = 0; i < 4 * 60; i++) {
1055 await Future.delayed(Duration(milliseconds: 250));
1056 await isBatteryOptimizationEnabled();
1057 }
1058 })());
1059 }
1060
1061 @action
1062 Future<void> enableBackgroundSync() async {
1063 if (hasBatteryOptimization && batteryOptimizationEnabled) {
1064 disableBackgroundSync();
1065 return;
1066 }
1067 final resp = await FlutterDaemon()
1068 .startBackgroundSync(settingsStore.currentSyncMode.frequency.inMinutes);
1069 printV("Background sync enabled: $resp");
1070 backgroundSyncEnabled = true;
1071 }
1072
1073 @action
1074 Future<void> disableBackgroundSync() async {
1075 final resp = await FlutterDaemon().stopBackgroundSync();
1076 printV("Background sync disabled: $resp");
1077 backgroundSyncEnabled = false;
1078 }
1079
1080 @computed
1081 bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore;
1082
1083 @action
1084 double getShadowSpread() {
1085 double spread = 0;
1086 if (!appStore.themeStore.currentTheme.isDark)
1087 spread = 0;
1088 else if (appStore.themeStore.currentTheme.isDark) spread = 0;
1089 return spread;
1090 }
1091
1092 @action
1093 double getShadowBlur() {
1094 double blur = 0;
1095 if (!appStore.themeStore.currentTheme.isDark)
1096 blur = 0;
1097 else if (appStore.themeStore.currentTheme.isDark) blur = 0;
1098 return blur;
1099 }
1100
1101 @action
1102 void setMwebEnabled() {
1103 if (!hasMweb) {
1104 return;
1105 }
1106
1107 settingsStore.hasEnabledMwebBefore = true;
1108 mwebEnabled = true;
1109 bitcoin!.setMwebEnabled(wallet, true);
1110 balanceViewModel.mwebEnabled = true;
1111 settingsStore.mwebAlwaysScan = true;
1112 }
1113
1114 @action
1115 void dismissMweb() {
1116 settingsStore.mwebCardDisplay = false;
1117 balanceViewModel.mwebEnabled = false;
1118 settingsStore.mwebAlwaysScan = false;
1119 mwebEnabled = false;
1120 bitcoin!.setMwebEnabled(wallet, false);
1121 }
1122
1123 @action
1124 Future<void> rescanInternalChangeZcash() async {
1125 await zcash!.rescanInternalChange(wallet);
1126 }
1127
1128 @action
1129 void dismissZcash() {
1130 settingsStore.showZcashMissingFundsCard = false;
1131 }
1132
1133 @action
1134 void dismissDecredInfoCard() {
1135 showDecredInfoCard = false;
1136 sharedPreferences.setBool(PreferencesKey.showDecredInfoCard, false);
1137 }
1138
1139 @action
1140 void dismissPayjoin() {
1141 settingsStore.showPayjoinCard = false;
1142 }
1143
1144 @action
1145 void enablePayjoin() {
1146 settingsStore.usePayjoin = true;
1147 settingsStore.showPayjoinCard = false;
1148 bitcoin!.updatePayjoinState(wallet, true);
1149 }
1150
1151 BalanceViewModel balanceViewModel;
1152
1153 TradeMonitor tradeMonitor;
1154
1155 AppStore appStore;
1156
1157 SettingsStore settingsStore;
1158
1159 YatStore yatStore;
1160
1161 TradesStore tradesStore;
1162
1163 OrdersStore ordersStore;
1164
1165 TradeFilterStore tradeFilterStore;
1166
1167 OrderFilterStore orderFilterStore;
1168
1169 AnonpayTransactionsStore anonpayTransactionsStore;
1170
1171 TransactionFilterStore transactionFilterStore;
1172
1173 PayjoinTransactionsStore payjoinTransactionsStore;
1174
1175 // Map<String, List<FilterItem>> filterItems;
1176
1177 List<FilterItem> filterItems;
1178
1179 List<FilterItem> exchangeFilterItems;
1180
1181 bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
1182
1183 bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
1184
1185 @action
1186 void furtherShowYatPopup(bool shouldShow) => settingsStore.shouldShowYatPopup = shouldShow;
1187
1188 @computed
1189 bool get isEnabledSwapAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled;
1190
1191 @computed
1192 bool get canSend => wallet.canSend();
1193
1194 @observable
1195 bool hasSwapAction;
1196
1197 @computed
1198 bool get isEnabledTradeAction => !settingsStore.disableTradeOption;
1199
1200 @observable
1201 bool hasTradeAction;
1202
1203 @computed
1204 bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
1205
1206 ReactionDisposer? _onMoneroAccountChangeReaction;
1207
1208 ReactionDisposer? _onMoneroBalanceChangeReaction;
1209
1210 ReactionDisposer? _transactionDisposer;
1211
1212 ReactionDisposer? _walletChangeDisposer;
1213
1214 ReactionDisposer? _chainChangeDisposer;
1215
1216 @computed
1217 bool get hasPowNodes => [WalletType.nano, WalletType.banano].contains(wallet.type);
1218
1219 @computed
1220 bool get hasSignMessages {
1221 if (wallet.isHardwareWallet) {
1222 return false;
1223 }
1224 switch (wallet.type) {
1225 case WalletType.monero:
1226 case WalletType.litecoin:
1227 case WalletType.bitcoin:
1228 case WalletType.bitcoinCash:
1229 case WalletType.ethereum:
1230 case WalletType.polygon:
1231 case WalletType.base:
1232 case WalletType.arbitrum:
1233 case WalletType.bsc:
1234 case WalletType.solana:
1235 case WalletType.nano:
1236 case WalletType.banano:
1237 case WalletType.tron:
1238 case WalletType.wownero:
1239 case WalletType.decred:
1240 case WalletType.dogecoin:
1241 return true;
1242 case WalletType.zano:
1243 case WalletType.haven:
1244 case WalletType.zcash:
1245 case WalletType.none:
1246 return false;
1247 }
1248 }
1249
1250 bool get showRepWarning {
1251 if (wallet.type != WalletType.nano) {
1252 return false;
1253 }
1254
1255 if (!settingsStore.shouldShowRepWarning) {
1256 return false;
1257 }
1258
1259 return !nano!.isRepOk(wallet);
1260 }
1261
1262 Future<void> reconnect() async {
1263 int? chainId;
1264 if (isEVMWallet) {
1265 chainId = evm!.getSelectedChainId(wallet);
1266 }
1267
1268 final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
1269 await wallet.connectToNode(node: node);
1270 if (hasPowNodes) {
1271 final powNode = settingsStore.getCurrentPowNode(wallet.type);
1272 await wallet.connectToPowNode(node: powNode);
1273 }
1274
1275 if (hasSilentPayments) {
1276 bitcoin!.setScanningActive(wallet, silentPaymentsScanningActive);
1277 }
1278 }
1279
1280 @action
1281 void _onWalletChange(
1282 WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
1283 if (wallet == null) {
1284 return;
1285 }
1286
1287 this.wallet = wallet;
1288 type = wallet.type;
1289 name = wallet.name;
1290 loadFilterItems();
1291
1292 addZcashMigrationReaction();
1293
1294 if (wallet.type == WalletType.monero) {
1295 subname = monero!.getCurrentAccount(wallet).label;
1296
1297 _onMoneroAccountChangeReaction?.reaction.dispose();
1298 _onMoneroBalanceChangeReaction?.reaction.dispose();
1299
1300 _onMoneroAccountChangeReaction = reaction(
1301 (_) => monero!.getMoneroWalletDetails(wallet).account,
1302 (Account account) => _onMoneroAccountChange(wallet));
1303
1304 _onMoneroBalanceChangeReaction = reaction(
1305 (_) => monero!.getMoneroWalletDetails(wallet).balance,
1306 (MoneroBalance balance) => _onMoneroTransactionsUpdate(wallet));
1307
1308 _onMoneroTransactionsUpdate(wallet);
1309 } else if (wallet.type == WalletType.wownero) {
1310 subname = wow.wownero!.getCurrentAccount(wallet).label;
1311
1312 _onMoneroAccountChangeReaction?.reaction.dispose();
1313 _onMoneroBalanceChangeReaction?.reaction.dispose();
1314
1315 _onMoneroAccountChangeReaction = reaction(
1316 (_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
1317 (wow.Account account) => _onMoneroAccountChange(wallet));
1318
1319 _onMoneroBalanceChangeReaction = reaction(
1320 (_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
1321 (wow.WowneroBalance balance) => _onMoneroTransactionsUpdate(wallet));
1322
1323 _onMoneroTransactionsUpdate(wallet);
1324 } else {
1325 // FIX-ME: Check for side effects
1326 // subname = null;
1327 subname = '';
1328
1329 _reloadTransactions();
1330 }
1331
1332 _transactionDisposer?.reaction.dispose();
1333
1334 if (isEVMCompatibleChain(wallet.type)) {
1335 _chainChangeDisposer?.reaction.dispose();
1336 _chainChangeDisposer = reaction((_) {
1337 // Access selectedChainId through proxy to track chain changes
1338 return evm!.getSelectedChainId(wallet);
1339 }, (_) {
1340 // When chain switches, reload transactions for the new chain
1341 _reloadTransactions();
1342 });
1343 } else {
1344 _chainChangeDisposer?.reaction.dispose();
1345 _chainChangeDisposer = null;
1346 }
1347
1348 _transactionDisposer = reaction((_) {
1349 final length = appStore.wallet!.transactionHistory.transactions.length;
1350 if (length == 0) {
1351 return 0;
1352 }
1353 int confirmations = 1;
1354 if (![WalletType.solana, WalletType.tron].contains(wallet.type)) {
1355 try {
1356 confirmations =
1357 appStore.wallet!.transactionHistory.transactions.values.first.confirmations +
1358 appStore.wallet!.transactionHistory.transactions.values.last.confirmations +
1359 1;
1360 } catch (_) {}
1361 } else {
1362 final pendingCount = appStore.wallet!.transactionHistory.transactions.values
1363 .where((item) => item.isPending)
1364 .length;
1365 confirmations = pendingCount + 1;
1366 }
1367 return length * confirmations;
1368 }, _transactionDisposerCallback, delay: 300);
1369 }
1370
1371 @action
1372 void _onMoneroAccountChange(WalletBase wallet) {
1373 if (wallet.type == WalletType.monero) {
1374 subname = monero!.getCurrentAccount(wallet).label;
1375 } else if (wallet.type == WalletType.wownero) {
1376 subname = wow.wownero!.getCurrentAccount(wallet).label;
1377 }
1378 _onMoneroTransactionsUpdate(wallet);
1379 }
1380
1381 @action
1382 void _onMoneroTransactionsUpdate(WalletBase wallet) {
1383 transactions.clear();
1384 if (wallet.type == WalletType.monero) {
1385 final _accountTransactions = monero!
1386 .getTransactionHistory(wallet)
1387 .transactions
1388 .values
1389 .where((tx) =>
1390 monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id)
1391 .toList();
1392
1393 transactions.addAll(
1394 _accountTransactions.map(
1395 (transaction) => TransactionListItem(
1396 transaction: transaction,
1397 balanceViewModel: balanceViewModel,
1398 appStore: appStore,
1399 key: ValueKey('monero_transaction_history_item_${transaction.id}_key'),
1400 ),
1401 ),
1402 );
1403 } else if (wallet.type == WalletType.wownero) {
1404 final _accountTransactions = wow.wownero!
1405 .getTransactionHistory(wallet)
1406 .transactions
1407 .values
1408 .where((tx) =>
1409 wow.wownero!.getTransactionInfoAccountId(tx) ==
1410 wow.wownero!.getCurrentAccount(wallet).id)
1411 .toList();
1412
1413 transactions.addAll(
1414 _accountTransactions.map(
1415 (transaction) => TransactionListItem(
1416 transaction: transaction,
1417 balanceViewModel: balanceViewModel,
1418 appStore: appStore,
1419 key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'),
1420 ),
1421 ),
1422 );
1423 }
1424 }
1425
1426 @action
1427 Future<void> setSyncMode(SyncMode syncMode) async {
1428 settingsStore.currentSyncMode = syncMode;
1429 await enableBackgroundSync();
1430 }
1431
1432 @computed
1433 bool get syncAll => settingsStore.currentSyncAll;
1434
1435 @computed
1436 bool get builtinTor => settingsStore.currentBuiltinTor;
1437
1438 @action
1439 void setBuiltinTor(bool value, BuildContext context) {
1440 if (value) {
1441 unawaited(
1442 showPopUp<bool>(
1443 context: context,
1444 builder: (BuildContext context) {
1445 return AlertWithOneAction(
1446 alertTitle: S.of(context).tor_connection,
1447 alertContent: S.of(context).tor_experimental,
1448 buttonText: S.of(context).ok,
1449 buttonAction: () => Navigator.of(context).pop(true),
1450 );
1451 },
1452 ),
1453 );
1454 }
1455 settingsStore.currentBuiltinTor = value;
1456 if (value) {
1457 unawaited(ensureTorStarted(context: context).then((_) async {
1458 if (settingsStore.currentBuiltinTor == false)
1459 return; // return when tor got disabled in the meantime;
1460 int? chainId;
1461 if (isEVMWallet) {
1462 chainId = evm!.getSelectedChainId(wallet);
1463 }
1464 await wallet.connectToNode(
1465 node: appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId));
1466 }));
1467 } else {
1468 unawaited(ensureTorStopped(context: context).then((_) async {
1469 if (settingsStore.currentBuiltinTor == true)
1470 return; // return when tor got enabled in the meantime;
1471 int? chainId;
1472 if (isEVMWallet) {
1473 chainId = evm!.getSelectedChainId(wallet);
1474 }
1475 await wallet.connectToNode(
1476 node: appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId));
1477 }));
1478 }
1479 }
1480
1481 @action
1482 void setSyncAll(bool value) => settingsStore.currentSyncAll = value;
1483
1484 Future<List<String>> checkForHavenWallets() async {
1485 final walletInfos = await WalletInfo.getAll();
1486 return walletInfos
1487 .where((element) => element.type == WalletType.haven)
1488 .map((e) => e.name)
1489 .toList();
1490 }
1491
1492 Future<List<String>> checkAffectedWallets() async {
1493 try {
1494 // await load file
1495 final vulnerableSeedsString = await rootBundle
1496 .loadString('assets/text/cakewallet_weak_bitcoin_seeds_hashed_sorted_version1.txt');
1497 final vulnerableSeeds = vulnerableSeedsString.split("\n");
1498
1499 List<String> affectedWallets = [];
1500 final walletInfos = await WalletInfo.getAll();
1501 for (var walletInfo in walletInfos) {
1502 if (walletInfo.type == WalletType.bitcoin) {
1503 final password = await keyService.getWalletPassword(walletName: walletInfo.name);
1504 final path = await pathForWallet(name: walletInfo.name, type: walletInfo.type);
1505 final jsonSource = await read(path: path, password: password);
1506 final data = json.decode(jsonSource) as Map;
1507 final mnemonic = data['mnemonic'] as String?;
1508
1509 if (mnemonic == null) continue;
1510
1511 final hash = await Cryptography.instance.sha256().hash(utf8.encode(mnemonic));
1512 final seedSha = bytesToHex(hash.bytes);
1513
1514 if (vulnerableSeeds.contains(seedSha)) {
1515 affectedWallets.add(walletInfo.name);
1516 }
1517 }
1518 }
1519
1520 return affectedWallets;
1521 } catch (_) {
1522 return [];
1523 }
1524 }
1525
1526 static ServicesResponse? cachedServicesResponse;
1527
1528 Future<ServicesResponse> getServicesStatus() async {
1529 if (cachedServicesResponse != null) {
1530 return cachedServicesResponse!;
1531 }
1532 cachedServicesResponse = await _getServicesStatus();
1533 return cachedServicesResponse!;
1534 }
1535
1536 Future<ServicesResponse> _getServicesStatus() async {
1537 try {
1538 if (isEnabledBulletinAction) {
1539 final res = await ProxyWrapper().get(
1540 clearnetUri: Uri.https(
1541 "service-api.cakewallet.com",
1542 "/v1/active-notices",
1543 {'key': secrets.fiatApiKey},
1544 ),
1545 onionUri: Uri.http(
1546 "jpirgl4lrwzjgdqj2nsv3g7twhp2efzty5d3cnypktyczzqfc5qcwwyd.onion",
1547 "/v1/active-notices",
1548 {'key': secrets.fiatApiKey},
1549 ),
1550 );
1551 if (res.statusCode < 200 || res.statusCode >= 300) {
1552 throw res.body;
1553 }
1554
1555 final oldSha = sharedPreferences.getString(PreferencesKey.serviceStatusShaKey);
1556
1557 final hash = await Cryptography.instance.sha256().hash(utf8.encode(res.body));
1558 final currentSha = bytesToHex(hash.bytes);
1559
1560 final hasUpdates = oldSha != currentSha;
1561
1562 return ServicesResponse.fromJson(
1563 json.decode(res.body) as Map<String, dynamic>,
1564 hasUpdates,
1565 currentSha,
1566 );
1567 } else {
1568 return ServicesResponse([], false, '');
1569 }
1570 } catch (e) {
1571 return ServicesResponse([], false, '');
1572 }
1573 }
1574
1575 @observable
1576 bool isMigratingToIronwood;
1577
1578 String getTransactionType(TransactionInfo tx) {
1579 if (wallet.type == WalletType.bitcoin) {
1580 if (tx.isReplaced == true) return ' (replaced)';
1581 }
1582
1583 if (wallet.chainId == 1 && tx.evmSignatureName == 'approval')
1584 return ' (${tx.evmSignatureName})';
1585
1586 return '';
1587 }
1588
1589 Future<void> refreshDashboard() async {
1590 reconnect();
1591 }
1592 }