Files
hash-wallet/lib/view_model/dashboard/dashboard_view_model.dart
T
jwinterm b57ab7cc06 Nuke Solana/Tron/Zano/Decred/Zcash/Haven from the codebase entirely
94 files changed, -5926 net lines. No stubs, no generators, no dead enum
values, no orphan switch branches. Removed for principled reasons (we
do not ship these chains) and to make the codebase smaller and friendlier
to contribute to.

What's gone:
- lib/{solana,tron,zano,decred,zcash,haven}/ directories
- lib/exchange/provider/jupiter_exchange_provider.dart (Solana-only)
- lib/entities/haven_seed_store.dart
- WalletType.{solana,tron,zano,decred,zcash,haven} enum values
  (HiveField IDs are pinned: Wownero stays at 12, all surviving types
  keep their IDs, so existing user-saved wallets deserialize unchanged)
- All ~70 lib/ files that imported the removed chain stubs or branched
  on the removed WalletType values
- backupSeeds(Box<HavenSeedStore>) abstract method + Wownero implementation
- Jupiter exchange branches in send/exchange view models
- TronGrid usage settings, Zano alias lookups, Zcash transparent address
  handling, Decred default-spv-nodes branches, Haven seed migration

What stays (intentional):
- CryptoCurrency.{xhv,sol,trx,dcr,zano,zec} enum entries in cw_core —
  they're still used as exchange/swap currency identifiers in
  address_validator.dart and elsewhere. The wallet types are gone but
  the *currencies* are still tradeable through Trocador.
- cw_core/spl_token.dart and cw_core/tron_token.dart Hive adapters in
  main.dart — the tokens still register as Hive types for backwards-
  compat with any old box data.
- lib/buy/{moonpay,robinhood,onramper,dfx,kryptonim,meld}/ provider
  source files (just stripped of solana/tron/etc references; providers
  themselves remain dormant per provider_types.dart).

Pinned-ID note: WalletType enum now has gaps in @HiveField IDs (4, 10,
11, 13, 14, 18 removed). This is deliberate — re-using a freed ID later
would mis-deserialize old hive boxes. Don't renumber.

Bundled config / CI hasn't changed; configure.dart was already free of
these chains in commit 4eb7db0b7.
2026-05-17 12:30:25 -04:00

1462 lines
48 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io' show Platform;
import 'package:hash_wallet/.secrets.g.dart' as secrets;
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/core/key_service.dart';
import "package:cw_core/balance_card_style_settings.dart";
import 'package:hash_wallet/core/trade_monitor.dart';
import 'package:hash_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:hash_wallet/entities/balance_display_mode.dart';
import 'package:hash_wallet/entities/exchange_api_mode.dart';
import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/entities/service_status.dart';
import 'package:hash_wallet/entities/sync_status_display_mode.dart';
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/utils/feature_flag.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/nano/nano.dart';
import 'package:hash_wallet/order/order_provider_description.dart';
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
import 'package:hash_wallet/store/dashboard/order_filter_store.dart';
import 'package:hash_wallet/utils/device_info.dart';
import 'package:hash_wallet/utils/show_pop_up.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:hash_wallet/utils/tor.dart';
import 'package:hash_wallet/wownero/wownero.dart' as wow;
import 'package:hash_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/dashboard/orders_store.dart';
import 'package:hash_wallet/store/dashboard/payjoin_transactions_store.dart';
import 'package:hash_wallet/store/dashboard/trade_filter_store.dart';
import 'package:hash_wallet/store/dashboard/trades_store.dart';
import 'package:hash_wallet/store/dashboard/transaction_filter_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/store/yat/yat_store.dart';
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:hash_wallet/view_model/dashboard/filter_item.dart';
import 'package:hash_wallet/view_model/dashboard/formatted_item_list.dart';
import 'package:hash_wallet/view_model/dashboard/order_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:hash_wallet/view_model/settings/sync_mode.dart';
import 'package:cryptography/cryptography.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_daemon/flutter_daemon.dart';
import 'package:mobx/mobx.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/evm/evm.dart';
part 'dashboard_view_model.g.dart';
class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
abstract class DashboardViewModelBase with Store {
DashboardViewModelBase(
{required this.balanceViewModel,
required this.tradeMonitor,
required this.appStore,
required this.tradesStore,
required this.tradeFilterStore,
required this.orderFilterStore,
required this.transactionFilterStore,
required this.settingsStore,
required this.yatStore,
required this.ordersStore,
required this.anonpayTransactionsStore,
required this.payjoinTransactionsStore,
required this.sharedPreferences,
required this.keyService})
: hasTradeAction = true,
hasSwapAction = true,
isShowFirstYatIntroduction = false,
isShowSecondYatIntroduction = false,
isShowThirdYatIntroduction = false,
filterItems = {
S.current.transactions: [
FilterItem(
value: () => transactionFilterStore.displayAll,
caption: S.current.all_transactions,
onChanged: transactionFilterStore.toggleAll),
FilterItem(
value: () => transactionFilterStore.displayIncoming,
caption: S.current.incoming,
onChanged: transactionFilterStore.toggleIncoming),
FilterItem(
value: () => transactionFilterStore.displayOutgoing,
caption: S.current.outgoing,
onChanged: transactionFilterStore.toggleOutgoing),
if (appStore.wallet!.type == WalletType.bitcoin)
FilterItem(
value: () => transactionFilterStore.displaySilentPayments,
caption: S.current.silent_payments,
onChanged: transactionFilterStore.toggleSilentPayments,
),
// FilterItem(
// value: () => false,
// caption: S.current.transactions_by_date,
// onChanged: null),
],
'Orders': [
FilterItem(
value: () => orderFilterStore.displayCakePay,
caption: 'Cake Pay',
onChanged: () =>
orderFilterStore.toggleDisplayOrder(OrderProviderDescription.cakePay)),
],
S.current.trades: [
FilterItem(
value: () => tradeFilterStore.displayAllTrades,
caption: S.current.all_trades,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
FilterItem(
value: () => tradeFilterStore.displayChangeNow,
caption: ExchangeProviderDescription.changeNow.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.changeNow)),
FilterItem(
value: () => tradeFilterStore.displaySideShift,
caption: ExchangeProviderDescription.sideShift.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.sideShift)),
FilterItem(
value: () => tradeFilterStore.displaySimpleSwap,
caption: ExchangeProviderDescription.simpleSwap.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)),
FilterItem(
value: () => tradeFilterStore.displayTrocador,
caption: ExchangeProviderDescription.trocador.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
FilterItem(
value: () => tradeFilterStore.displayExolix,
caption: ExchangeProviderDescription.exolix.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
FilterItem(
value: () => tradeFilterStore.displayChainflip,
caption: ExchangeProviderDescription.chainflip.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)),
FilterItem(
value: () => tradeFilterStore.displayThorChain,
caption: ExchangeProviderDescription.thorChain.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)),
FilterItem(
value: () => tradeFilterStore.displayLetsExchange,
caption: ExchangeProviderDescription.letsExchange.title,
onChanged: () => tradeFilterStore
.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)),
FilterItem(
value: () => tradeFilterStore.displayStealthEx,
caption: ExchangeProviderDescription.stealthEx.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)),
FilterItem(
value: () => tradeFilterStore.displayXOSwap,
caption: ExchangeProviderDescription.xoSwap.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)),
FilterItem(
value: () => tradeFilterStore.displaySwapTrade,
caption: ExchangeProviderDescription.swapTrade.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapTrade)),
FilterItem(
value: () => tradeFilterStore.displaySwapXyz,
caption: ExchangeProviderDescription.swapsXyz.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)),
FilterItem(
value: () => tradeFilterStore.displayNearIntents,
caption: ExchangeProviderDescription.nearIntents.title,
onChanged: () => tradeFilterStore
.toggleDisplayExchange(ExchangeProviderDescription.nearIntents)),
]
},
subname = '',
name = appStore.wallet!.name,
type = appStore.wallet!.type,
transactions = ObservableList<TransactionListItem>(),
cardDesigns = ObservableList<CardDesign>(),
cardOrder = ObservableMap<int, int>(),
wallet = appStore.wallet! {
showDecredInfoCard = false;
name = wallet.name;
type = wallet.type;
isShowFirstYatIntroduction = false;
isShowSecondYatIntroduction = false;
isShowThirdYatIntroduction = false;
unawaited(isBackgroundSyncEnabled());
unawaited(isBatteryOptimizationEnabled());
unawaited(_loadConstraints());
final _wallet = wallet;
if (_wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).balance,
(MoneroBalance balance) => _onMoneroTransactionsUpdate(_wallet));
final _accountTransactions = _wallet.transactionHistory.transactions.values
.where((tx) =>
monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id)
.toList();
final sortedTransactions = [..._accountTransactions];
sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
transactions = ObservableList.of(
sortedTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('monero_transaction_history_item_${transaction.id}_key'),
),
),
);
} else if (_wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
(wow.Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
(wow.WowneroBalance balance) => _onMoneroTransactionsUpdate(_wallet));
final _accountTransactions = _wallet.transactionHistory.transactions.values
.where((tx) =>
wow.wownero!.getTransactionInfoAccountId(tx) ==
wow.wownero!.getCurrentAccount(wallet).id)
.toList();
final sortedTransactions = [..._accountTransactions];
sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
transactions = ObservableList.of(
sortedTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'),
),
),
);
} else {
final sortedTransactions = [...wallet.transactionHistory.transactions.values];
sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
transactions = ObservableList.of(
sortedTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'),
),
),
);
}
// TODO: nano sub-account generation is disabled:
// if (_wallet.type == WalletType.nano || _wallet.type == WalletType.banano) {
// subname = nano!.getCurrentAccount(_wallet).label;
// }
_walletChangeDisposer?.reaction.dispose();
_walletChangeDisposer = reaction((_) => appStore.wallet, (wallet) {
_onWalletChange(wallet);
_checkMweb();
loadCardDesigns();
showDecredInfoCard = false;
tradeMonitor.stopTradeMonitoring();
tradeMonitor.monitorActiveTrades(wallet!.id);
});
_transactionDisposer?.reaction.dispose();
_transactionDisposer = reaction((_) {
final length = appStore.wallet!.transactionHistory.transactions.length;
if (length == 0) {
return 0;
}
int confirmations = 1;
try {
confirmations =
appStore.wallet!.transactionHistory.transactions.values.first.confirmations +
appStore.wallet!.transactionHistory.transactions.values.last.confirmations +
1;
} catch (_) {}
return length * confirmations;
}, _transactionDisposerCallback, delay: 300);
if (hasSilentPayments) {
silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
reaction((_) => wallet.syncStatus, (SyncStatus syncStatus) {
silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
});
}
loadCardDesigns();
_checkMweb();
reaction((_) => settingsStore.mwebAlwaysScan, (bool value) => _checkMweb());
reaction((_) => tradesStore.trades, (_) => tradeMonitor.monitorActiveTrades(wallet.id));
tradeMonitor.monitorActiveTrades(wallet.id);
}
bool _isTransactionDisposerCallbackRunning = false;
@action
void _reloadTransactions() {
if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero) {
return; // Monero/Wownero transactions are handled separately
}
transactions.clear();
transactions.addAll(
wallet.transactionHistory.transactions.values.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('${wallet.type.name}_transaction_history_item_${transaction.id}_key'),
),
),
);
}
@computed
bool get isSyncHeavy {
if ([
WalletType.monero,
WalletType.wownero,
].contains(wallet.type)) {
return true;
}
if (wallet.type == WalletType.bitcoin && silentPaymentsScanningActive && hasSilentPayments) {
return true;
}
if (wallet.type == WalletType.litecoin && mwebEnabled && hasMweb) {
return true;
}
return false;
}
bool showBridge(CryptoCurrency currency) {
if(!isEVMCompatibleChain(wallet.type)) return false;
if(evm!.isUSDT0Token(wallet, currency)) return true;
return false;
}
@action
Future<void> loadCardDesigns() async {
final accountStyleSettings =
await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
late final int numAccounts;
if (wallet.type == WalletType.monero) {
numAccounts = monero!.getAccountList(wallet).accounts.length;
} else if (wallet.type == WalletType.wownero) {
numAccounts = wow.wownero!.getAccountList(wallet).accounts.length;
} else if (wallet.type == WalletType.bitcoin) {
// bitcoin and lightning
numAccounts = 2;
} else {
numAccounts = 1;
}
cardDesigns.clear();
Map<int, int> newOrder = {};
for (int i = 0; i < numAccounts; i++) {
late final int index;
if (balanceViewModel.hasAccounts) {
index = i;
} else if (wallet.type == WalletType.bitcoin && i == 1) {
index = 0;
} else {
index = -1;
}
final setting = accountStyleSettings.where((e) => e.accountIndex == index).firstOrNull;
late final CryptoCurrency curr;
if (wallet.type == WalletType.bitcoin && i == 1) {
curr = CryptoCurrency.btcln;
} else {
curr = wallet.currency;
}
cardDesigns.add(CardDesign.fromStyleSettings(setting, curr));
if (setting?.cardOrder != null) {
newOrder[setting!.cardOrder] = i;
}
}
// making sure ALL accounts have numbers, even the ones that existed before this feature was a thing
for (int i = 0; i < numAccounts; i++) {
if (!newOrder.containsKey(i) && !(wallet.type != WalletType.bitcoin && i == 1)) {
int free = 0;
while (newOrder.containsValue(free)) {
free++;
}
if (wallet.type == WalletType.bitcoin) {
newOrder[free] = 0;
} else {
newOrder[free] = i;
}
}
}
cardOrder = newOrder.asObservable();
}
void _transactionDisposerCallback(int _) async {
// Simple check to prevent the callback from being called multiple times in the same frame
if (_isTransactionDisposerCallbackRunning) return;
_isTransactionDisposerCallbackRunning = true;
await Future.delayed(Duration.zero);
try {
final currentAccountId = wallet.type == WalletType.monero
? monero!.getCurrentAccount(wallet).id
: wallet.type == WalletType.wownero
? wow.wownero!.getCurrentAccount(wallet).id
: null;
final List<TransactionInfo> relevantTxs = [];
for (final tx in appStore.wallet!.transactionHistory.transactions.values) {
bool isRelevant = true;
if (wallet.type == WalletType.monero) {
isRelevant = monero!.getTransactionInfoAccountId(tx) == currentAccountId;
} else if (wallet.type == WalletType.wownero) {
isRelevant = wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId;
}
if (isRelevant) {
relevantTxs.add(tx);
}
}
// printV("Transaction disposer callback (relevantTxs: ${relevantTxs.length} current: ${transactions.length})");
String _txIdentityString(String txHash, TransactionDirection direction) => "${txHash}_$direction";
String _txIdentityStringConfirmations(String txHash, TransactionDirection direction, int confirmations) => "${txHash}_${direction}_$confirmations";
final existingKeys = transactions
.map((item) => _txIdentityStringConfirmations(item.transaction.txHash, item.transaction.direction, item.transaction.confirmations))
.toSet();
final newTransactions = relevantTxs
.where((tx) => !existingKeys.contains(_txIdentityStringConfirmations(tx.txHash, tx.direction, tx.confirmations)))
.map((tx) => TransactionListItem(
transaction: tx,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
))
.toList();
final newKeys = newTransactions
.map((item) => _txIdentityString(item.transaction.txHash, item.transaction.direction))
.toSet();
transactions.removeWhere(
(item) => newKeys.contains(_txIdentityString(item.transaction.txHash, item.transaction.direction)),
);
transactions.addAll(newTransactions);
// transactions.clear();
// transactions.addAll(relevantTxs.map((tx) => TransactionListItem(
// transaction: tx,
// balanceViewModel: balanceViewModel,
// appStore: appStore,
// key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'),
// )));
} finally {
_isTransactionDisposerCallbackRunning = false;
}
}
void _checkMweb() {
if (hasMweb) {
mwebEnabled = bitcoin!.getMwebEnabled(wallet);
balanceViewModel.mwebEnabled = mwebEnabled;
}
}
@observable
WalletType type;
@observable
String name;
@observable
ObservableList<TransactionListItem> transactions;
@observable
String subname;
@observable
bool isShowFirstYatIntroduction;
@observable
bool isShowSecondYatIntroduction;
@observable
bool isShowThirdYatIntroduction;
@observable
ObservableList<CardDesign> cardDesigns;
@observable
ObservableMap<int, int> cardOrder;
@computed
bool get isDarkTheme => appStore.themeStore.currentTheme.isDark;
@computed
String get address => wallet.walletAddresses.address;
@computed
bool get isTorEnabled => settingsStore.currentBuiltinTor;
@computed
SyncStatus get status => wallet.syncStatus;
@computed
bool get shouldShowMwebAd {
if (wallet.type != WalletType.litecoin) return false;
if (mwebEnabled) return false;
if (settingsStore.mwebAdDismissed) return false;
return Platform.isAndroid || Platform.isIOS;
}
@action
void dismissMwebAd(bool enableMweb) {
if (enableMweb) setMwebEnabled();
settingsStore.mwebAdDismissed = true;
}
@computed
String get syncStatusText {
var statusText = '';
if (status is SyncingSyncStatus) {
statusText = S.current.Blocks_remaining(status.toString());
}
if (status is FailedSyncStatus || status is LostConnectionSyncStatus) {
statusText = S.current.please_try_to_connect_to_another_node;
}
if (status is ProcessingSyncStatus) {
statusText = (status as ProcessingSyncStatus).message ?? S.current.processing;
}
return statusText;
}
@computed
double get confirmationProgress {
int received = 0;
int needed = 0;
for (final transaction in transactions) {
if (transaction.neededConfirmations == 0) {
continue;
}
if (transaction.transaction.confirmations >= transaction.neededConfirmations) {
continue;
}
received += transaction.transaction.confirmations;
needed += transaction.neededConfirmations;
}
if (needed == 0) {
return 1;
}
return received / needed;
}
@computed
BalanceDisplayMode get balanceDisplayMode => appStore.settingsStore.balanceDisplayMode;
@computed
@Deprecated("Replaced by showApps")
bool get shouldShowMarketPlaceInDashboard =>
appStore.settingsStore.shouldShowMarketPlaceInDashboard;
@computed
bool get showApps => appStore.settingsStore.shouldShowMarketPlaceInDashboard;
@computed
List<TradeListItem> get trades => tradesStore.trades.where((trade) {
final isSameChain = trade.trade.chainId != null
? trade.trade.chainId == wallet.chainId
: true; // returning default as true here so it falls back to the default checks if there's no chainId
return trade.trade.walletId == wallet.id && isSameChain;
}).toList();
@computed
bool get shouldShowBalanceHiddenMessage => balanceDisplayMode == BalanceDisplayMode.hiddenBalance && appStore.settingsStore.balanceHideCounter < 10;
@computed
List<OrderListItem> get orders =>
ordersStore.orders.where((item) => item.order.walletId == wallet.id).toList();
@computed
List<AnonpayTransactionListItem> get anonpayTransactions => anonpayTransactionsStore.transactions
.where((item) => item.transaction.walletId == wallet.id)
.toList();
@computed
List<PayjoinTransactionListItem> get payjoinTransactions => payjoinTransactionsStore.transactions
.where((item) => item.session.walletId == wallet.id)
.toList();
@computed
double get price => balanceViewModel.price;
@computed
bool get isAutoGenerateSubaddressesEnabled =>
settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled;
@computed
List<ActionListItem> get items {
final _items = <ActionListItem>[];
_items.addAll(
transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactions]));
_items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
_items.addAll(orderFilterStore.filtered(orders: orders, wallet: wallet));
if (payjoinTransactions.isNotEmpty) {
final _payjoinTransactions = payjoinTransactions;
_items.forEach((e) {
if (e is TransactionListItem &&
_payjoinTransactions.any((t) => t.session.txId == e.transaction.id)) {
_payjoinTransactions.firstWhere((t) => t.session.txId == e.transaction.id).transaction =
e.transaction;
}
});
_items.addAll(_payjoinTransactions);
_items.removeWhere((e) => (e is TransactionListItem &&
_payjoinTransactions.any((t) => t.session.txId == e.transaction.id)));
}
return formattedItemsList(_items);
}
@observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
@computed
bool get hasLightning =>
FeatureFlag.isLightningEnabled &&
wallet.type == WalletType.bitcoin &&
wallet.isSoftwareWallet &&
bitcoin!.useLightning(wallet);
@computed
bool get hasWalletConnect =>
isWalletConnectCompatibleChain(wallet.type) && !wallet.isHardwareWallet;
@computed
bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet);
@computed
bool get hasRescan => wallet.hasRescan;
@computed
bool get hasBackgroundSync => [
WalletType.monero,
].contains(wallet.type);
@computed
bool get isMoneroViewOnly {
if (wallet.type != WalletType.monero) return false;
return monero!.isViewOnly();
}
@computed
String? get getMoneroError {
if (wallet.type != WalletType.monero) return null;
try {
monero!.monerocCheck();
} catch (e) {
return e.toString();
}
return null;
}
@computed
String? get getWowneroError {
if (wallet.type != WalletType.wownero) return null;
try {
wow.wownero!.wownerocCheck();
} catch (e) {
return e.toString();
}
return null;
}
List<String> get isMoneroWalletBrokenReasons {
if (wallet.type != WalletType.monero) return [];
final keys = monero!.getKeys(wallet);
List<String> errors = [
// leaving these commented out for now, I'll be able to fix that properly in the airgap update
// to not cause work duplication, this will do the job as well, it will be slightly less precise
// about what happened - but still enough.
// if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0",
if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("") &&
!wallet.isHardwareWallet)
"private view key is 0",
// if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0",
if (keys['publicViewKey'] == List.generate(64, (index) => "0").join(""))
"public view key is 0",
// if (wallet.seed == null) "wallet seed is null",
// if (wallet.seed == "") "wallet seed is empty",
// if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address ==
// "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4")
// "primary address is invalid, you won't be able to receive / spend funds",
];
return errors;
}
@computed
bool get showZcashMissingFundsCard => false;
@computed
bool get hasSilentPayments =>
wallet.type == WalletType.bitcoin &&
(bitcoin!.getWalletKeys(wallet)["privateKey"] ?? "").isNotEmpty &&
!wallet.isHardwareWallet;
@computed
bool get showSilentPaymentsCard => hasSilentPayments && settingsStore.silentPaymentsCardDisplay;
@computed
bool get isEVMWallet => isEVMCompatibleChain(wallet.type);
@computed
List<ChainInfo> get availableChains {
if (!isEVMWallet) return [];
return evm!.getAllChains();
}
@computed
ChainInfo? get currentChain {
if (!isEVMWallet) return null;
return evm!.getCurrentChain(wallet);
}
@action
Future<void> selectChain(int chainId) async {
if (!isEVMWallet) return;
final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
await evm!.selectChain(wallet, chainId, node: node);
}
final KeyService keyService;
final SharedPreferences sharedPreferences;
@observable
bool silentPaymentsScanningActive = false;
@action
void setSilentPaymentsScanning(bool active) {
silentPaymentsScanningActive = active;
if (hasSilentPayments) {
bitcoin!.setScanningActive(wallet, active);
}
}
@computed
bool get hasMweb =>
wallet.type == WalletType.litecoin &&
(Platform.isIOS || Platform.isAndroid) &&
!wallet.isHardwareWallet;
@computed
bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled;
@observable
bool mwebEnabled = false;
@observable
late bool showDecredInfoCard;
@computed
bool get showPayjoinCard =>
wallet.type == WalletType.bitcoin &&
settingsStore.showPayjoinCard &&
!settingsStore.usePayjoin &&
DeviceInfo.instance.isMobile;
@observable
bool backgroundSyncEnabled = false;
@action
Future<bool> isBackgroundSyncEnabled() async {
if (!Platform.isAndroid) {
return false;
}
final resp = await FlutterDaemon().getBackgroundSyncStatus();
backgroundSyncEnabled = resp;
return resp;
}
@action
void toggleSwitchStatusDisplayMode() {
if (status is SyncingSyncStatus &&
!((status as SyncingSyncStatus).shouldShowBlocksRemaining())) {
if (settingsStore.syncStatusDisplayMode == SyncStatusDisplayMode.eta) {
settingsStore.syncStatusDisplayMode = SyncStatusDisplayMode.blocksRemaining;
} else {
settingsStore.syncStatusDisplayMode = SyncStatusDisplayMode.eta;
}
}
}
@observable
late bool backgroundSyncNotificationsEnabled =
sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false;
@action
Future<void> setBackgroundSyncNotificationsEnabled(bool value) async {
if (!value) {
backgroundSyncNotificationsEnabled = false;
sharedPreferences.setBool(PreferencesKey.backgroundSyncNotificationsEnabled, false);
return;
}
PermissionStatus permissionStatus = await Permission.notification.status;
if (permissionStatus != PermissionStatus.granted) {
final resp = await Permission.notification.request();
if (resp == PermissionStatus.denied) {
throw Exception("Notification permission denied");
}
}
backgroundSyncNotificationsEnabled = value;
await sharedPreferences.setBool(PreferencesKey.backgroundSyncNotificationsEnabled, value);
}
bool get hasBgsyncNetworkConstraints => Platform.isAndroid;
bool get hasBgsyncBatteryNotLowConstraints => Platform.isAndroid;
bool get hasBgsyncChargingConstraints => Platform.isAndroid;
bool get hasBgsyncDeviceIdleConstraints => Platform.isAndroid;
@observable
bool backgroundSyncNetworkUnmetered = false;
@observable
bool backgroundSyncBatteryNotLow = false;
@observable
bool backgroundSyncCharging = false;
@observable
bool backgroundSyncDeviceIdle = false;
Future<void> _loadConstraints() async {
if (Platform.isAndroid) {
backgroundSyncNetworkUnmetered = await FlutterDaemon().getNetworkType();
backgroundSyncBatteryNotLow = await FlutterDaemon().getBatteryNotLow();
backgroundSyncCharging = await FlutterDaemon().getRequiresCharging();
backgroundSyncDeviceIdle = await FlutterDaemon().getDeviceIdle();
}
}
@action
Future<void> setBackgroundSyncNetworkUnmetered(bool value) async {
backgroundSyncNetworkUnmetered = value;
await FlutterDaemon().setNetworkType(value);
if (await isBackgroundSyncEnabled()) {
await enableBackgroundSync();
}
}
@action
Future<void> setBackgroundSyncBatteryNotLow(bool value) async {
backgroundSyncBatteryNotLow = value;
await FlutterDaemon().setBatteryNotLow(value);
if (await isBackgroundSyncEnabled()) {
await enableBackgroundSync();
}
}
@action
Future<void> setBackgroundSyncCharging(bool value) async {
backgroundSyncCharging = value;
await FlutterDaemon().setRequiresCharging(value);
if (await isBackgroundSyncEnabled()) {
await enableBackgroundSync();
}
}
@action
Future<void> setBackgroundSyncDeviceIdle(bool value) async {
backgroundSyncDeviceIdle = value;
await FlutterDaemon().setDeviceIdle(value);
if (await isBackgroundSyncEnabled()) {
await enableBackgroundSync();
}
}
bool get hasBatteryOptimization => Platform.isAndroid;
@observable
bool batteryOptimizationEnabled = false;
@action
Future<bool> isBatteryOptimizationEnabled() async {
if (!hasBatteryOptimization) {
return false;
}
final resp = await FlutterDaemon().isBatteryOptimizationDisabled();
batteryOptimizationEnabled = !resp;
if (batteryOptimizationEnabled && await isBackgroundSyncEnabled()) {
// If the battery optimization is enabled, we need to disable the background sync
await disableBackgroundSync();
}
return resp;
}
@action
Future<void> disableBatteryOptimization() async {
final resp = await FlutterDaemon().requestDisableBatteryOptimization();
unawaited((() async {
// android doesn't return if the permission was granted, so we need to poll it,
// minute should be enough for the fallback method (opening settings and changing the permission)
for (var i = 0; i < 4 * 60; i++) {
await Future.delayed(Duration(milliseconds: 250));
await isBatteryOptimizationEnabled();
}
})());
}
@action
Future<void> enableBackgroundSync() async {
if (hasBatteryOptimization && batteryOptimizationEnabled) {
disableBackgroundSync();
return;
}
final resp = await FlutterDaemon()
.startBackgroundSync(settingsStore.currentSyncMode.frequency.inMinutes);
printV("Background sync enabled: $resp");
backgroundSyncEnabled = true;
}
@action
Future<void> disableBackgroundSync() async {
final resp = await FlutterDaemon().stopBackgroundSync();
printV("Background sync disabled: $resp");
backgroundSyncEnabled = false;
}
@computed
bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore;
@action
double getShadowSpread() {
double spread = 0;
if (!appStore.themeStore.currentTheme.isDark)
spread = 0;
else if (appStore.themeStore.currentTheme.isDark) spread = 0;
return spread;
}
@action
double getShadowBlur() {
double blur = 0;
if (!appStore.themeStore.currentTheme.isDark)
blur = 0;
else if (appStore.themeStore.currentTheme.isDark) blur = 0;
return blur;
}
@action
void setMwebEnabled() {
if (!hasMweb) {
return;
}
settingsStore.hasEnabledMwebBefore = true;
mwebEnabled = true;
bitcoin!.setMwebEnabled(wallet, true);
balanceViewModel.mwebEnabled = true;
settingsStore.mwebAlwaysScan = true;
}
@action
void dismissMweb() {
settingsStore.mwebCardDisplay = false;
balanceViewModel.mwebEnabled = false;
settingsStore.mwebAlwaysScan = false;
mwebEnabled = false;
bitcoin!.setMwebEnabled(wallet, false);
}
@action
void dismissZcash() {
settingsStore.showZcashMissingFundsCard = false;
}
@action
void dismissDecredInfoCard() {
showDecredInfoCard = false;
sharedPreferences.setBool(PreferencesKey.showDecredInfoCard, false);
}
@action
void dismissPayjoin() {
settingsStore.showPayjoinCard = false;
}
@action
void enablePayjoin() {
settingsStore.usePayjoin = true;
settingsStore.showPayjoinCard = false;
bitcoin!.updatePayjoinState(wallet, true);
}
BalanceViewModel balanceViewModel;
TradeMonitor tradeMonitor;
AppStore appStore;
SettingsStore settingsStore;
YatStore yatStore;
TradesStore tradesStore;
OrdersStore ordersStore;
TradeFilterStore tradeFilterStore;
OrderFilterStore orderFilterStore;
AnonpayTransactionsStore anonpayTransactionsStore;
TransactionFilterStore transactionFilterStore;
PayjoinTransactionsStore payjoinTransactionsStore;
Map<String, List<FilterItem>> filterItems;
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
@action
void furtherShowYatPopup(bool shouldShow) => settingsStore.shouldShowYatPopup = shouldShow;
@computed
bool get isEnabledSwapAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled;
@computed
bool get canSend => wallet.canSend();
@observable
bool hasSwapAction;
@computed
bool get isEnabledTradeAction => !settingsStore.disableTradeOption;
@observable
bool hasTradeAction;
@computed
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
ReactionDisposer? _onMoneroAccountChangeReaction;
ReactionDisposer? _onMoneroBalanceChangeReaction;
ReactionDisposer? _transactionDisposer;
ReactionDisposer? _walletChangeDisposer;
ReactionDisposer? _chainChangeDisposer;
@computed
bool get hasPowNodes => [WalletType.nano, WalletType.banano].contains(wallet.type);
@computed
bool get hasSignMessages {
if (wallet.isHardwareWallet) {
return false;
}
switch (wallet.type) {
case WalletType.monero:
case WalletType.litecoin:
case WalletType.bitcoin:
case WalletType.bitcoinCash:
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.nano:
case WalletType.banano:
case WalletType.wownero:
case WalletType.dogecoin:
return true;
case WalletType.none:
return false;
}
}
bool get showRepWarning {
if (wallet.type != WalletType.nano) {
return false;
}
if (!settingsStore.shouldShowRepWarning) {
return false;
}
return !nano!.isRepOk(wallet);
}
Future<void> reconnect() async {
int? chainId;
if (isEVMWallet) {
chainId = evm!.getSelectedChainId(wallet);
}
final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
await wallet.connectToNode(node: node);
if (hasPowNodes) {
final powNode = settingsStore.getCurrentPowNode(wallet.type);
await wallet.connectToPowNode(node: powNode);
}
if (hasSilentPayments) {
bitcoin!.setScanningActive(wallet, silentPaymentsScanningActive);
}
}
@action
void _onWalletChange(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
if (wallet == null) {
return;
}
this.wallet = wallet;
type = wallet.type;
name = wallet.name;
if (wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(wallet).label;
_onMoneroAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).balance,
(MoneroBalance balance) => _onMoneroTransactionsUpdate(wallet));
_onMoneroTransactionsUpdate(wallet);
} else if (wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(wallet).label;
_onMoneroAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
(wow.Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
(wow.WowneroBalance balance) => _onMoneroTransactionsUpdate(wallet));
_onMoneroTransactionsUpdate(wallet);
} else {
// FIX-ME: Check for side effects
// subname = null;
subname = '';
_reloadTransactions();
}
_transactionDisposer?.reaction.dispose();
if (isEVMCompatibleChain(wallet.type)) {
_chainChangeDisposer?.reaction.dispose();
_chainChangeDisposer = reaction((_) {
// Access selectedChainId through proxy to track chain changes
return evm!.getSelectedChainId(wallet);
}, (_) {
// When chain switches, reload transactions for the new chain
_reloadTransactions();
});
} else {
_chainChangeDisposer?.reaction.dispose();
_chainChangeDisposer = null;
}
_transactionDisposer = reaction((_) {
final length = appStore.wallet!.transactionHistory.transactions.length;
if (length == 0) {
return 0;
}
int confirmations = 1;
try {
confirmations =
appStore.wallet!.transactionHistory.transactions.values.first.confirmations +
appStore.wallet!.transactionHistory.transactions.values.last.confirmations +
1;
} catch (_) {}
return length * confirmations;
}, _transactionDisposerCallback, delay: 300);
}
@action
void _onMoneroAccountChange(WalletBase wallet) {
if (wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(wallet).label;
} else if (wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(wallet).label;
}
_onMoneroTransactionsUpdate(wallet);
}
@action
void _onMoneroTransactionsUpdate(WalletBase wallet) {
transactions.clear();
if (wallet.type == WalletType.monero) {
final _accountTransactions = monero!
.getTransactionHistory(wallet)
.transactions
.values
.where((tx) =>
monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id)
.toList();
transactions.addAll(
_accountTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('monero_transaction_history_item_${transaction.id}_key'),
),
),
);
} else if (wallet.type == WalletType.wownero) {
final _accountTransactions = wow.wownero!
.getTransactionHistory(wallet)
.transactions
.values
.where((tx) =>
wow.wownero!.getTransactionInfoAccountId(tx) ==
wow.wownero!.getCurrentAccount(wallet).id)
.toList();
transactions.addAll(
_accountTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'),
),
),
);
}
}
@action
Future<void> setSyncMode(SyncMode syncMode) async {
settingsStore.currentSyncMode = syncMode;
await enableBackgroundSync();
}
@computed
bool get syncAll => settingsStore.currentSyncAll;
@computed
bool get builtinTor => settingsStore.currentBuiltinTor;
@action
void setBuiltinTor(bool value, BuildContext context) {
if (value) {
unawaited(
showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).tor_connection,
alertContent: S.of(context).tor_experimental,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(true),
);
},
),
);
}
settingsStore.currentBuiltinTor = value;
if (value) {
unawaited(ensureTorStarted(context: context).then((_) async {
if (settingsStore.currentBuiltinTor == false)
return; // return when tor got disabled in the meantime;
int? chainId;
if (isEVMWallet) {
chainId = evm!.getSelectedChainId(wallet);
}
await wallet.connectToNode(
node: appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId));
}));
} else {
unawaited(ensureTorStopped(context: context).then((_) async {
if (settingsStore.currentBuiltinTor == true)
return; // return when tor got enabled in the meantime;
int? chainId;
if (isEVMWallet) {
chainId = evm!.getSelectedChainId(wallet);
}
await wallet.connectToNode(
node: appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId));
}));
}
}
@action
void setSyncAll(bool value) => settingsStore.currentSyncAll = value;
Future<List<String>> checkAffectedWallets() async {
try {
// await load file
final vulnerableSeedsString = await rootBundle
.loadString('assets/text/cakewallet_weak_bitcoin_seeds_hashed_sorted_version1.txt');
final vulnerableSeeds = vulnerableSeedsString.split("\n");
List<String> affectedWallets = [];
final walletInfos = await WalletInfo.getAll();
for (var walletInfo in walletInfos) {
if (walletInfo.type == WalletType.bitcoin) {
final password = await keyService.getWalletPassword(walletName: walletInfo.name);
final path = await pathForWallet(name: walletInfo.name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
if (mnemonic == null) continue;
final hash = await Cryptography.instance.sha256().hash(utf8.encode(mnemonic));
final seedSha = bytesToHex(hash.bytes);
if (vulnerableSeeds.contains(seedSha)) {
affectedWallets.add(walletInfo.name);
}
}
}
return affectedWallets;
} catch (_) {
return [];
}
}
static ServicesResponse? cachedServicesResponse;
Future<ServicesResponse> getServicesStatus() async {
if (cachedServicesResponse != null) {
return cachedServicesResponse!;
}
cachedServicesResponse = await _getServicesStatus();
return cachedServicesResponse!;
}
Future<ServicesResponse> _getServicesStatus() async {
// Hash Wallet: bulletin endpoint stubbed. The original implementation
// polled Cake's service-api.cakewallet.com/v1/active-notices (gated by
// the Cake-issued fiatApiKey) to surface push notices to all wallets.
// We don't run our own bulletin service yet. To restore: stand up an
// equivalent endpoint (likely under hash.boats or such.software) that
// returns the same JSON shape as service_status.dart expects, and put
// the network call back in place. Keep the user-facing toggle alive
// for forward-compat with the settings UI.
return ServicesResponse([], false, '');
}
String getTransactionType(TransactionInfo tx) {
if (wallet.type == WalletType.bitcoin) {
if (tx.isReplaced == true) return ' (replaced)';
}
if (wallet.chainId == 1 && tx.evmSignatureName == 'approval')
return ' (${tx.evmSignatureName})';
return '';
}
Future<void> refreshDashboard() async {
reconnect();
}
}