Merge upstream Cake Wallet v6.5.0

Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
@@ -1,32 +0,0 @@
enum ActionListDisplayMode { transactions, trades }
int serializeActionlistDisplayModes(List<ActionListDisplayMode> modes) {
var i = 0;
for (final mode in modes) {
switch (mode) {
case ActionListDisplayMode.trades:
i += 1;
break;
case ActionListDisplayMode.transactions:
i += 10;
break;
}
}
return i;
}
List<ActionListDisplayMode> deserializeActionlistDisplayModes(int raw) {
final modes = <ActionListDisplayMode>[];
if (raw == 1 || raw - 10 == 1) {
modes.add(ActionListDisplayMode.trades);
}
if (raw >= 10) {
modes.add(ActionListDisplayMode.transactions);
}
return modes;
}
+124 -91
View File
@@ -3,10 +3,13 @@ import 'package:hash_wallet/core/utilities.dart';
import 'package:hash_wallet/entities/balance_display_mode.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
import 'package:hash_wallet/entities/fiat_api_mode.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/entities/sort_balance_types.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/wallet_base.dart';
@@ -25,47 +28,74 @@ part 'balance_view_model.g.dart';
class BalanceRecord {
const BalanceRecord({
required this.raw,
required this.availableBalance,
required this.additionalBalance,
required this.secondAvailableBalance,
required this.secondAdditionalBalance,
required this.frozenBalance,
required this.fiatAvailableBalance,
required this.fiatAdditionalBalance,
required this.fiatFrozenBalance,
required this.fiatSecondAvailableBalance,
required this.fiatSecondAdditionalBalance,
required this.fiatAvailableBalanceRaw,
required this.fiatAdditionalBalanceRaw,
required this.fiatFrozenBalanceRaw,
required this.fiatSecondAvailableBalanceRaw,
required this.fiatSecondAdditionalBalanceRaw,
required this.asset,
required this.secondAsset,
required this.fiatCurrency,
required this.formattedAssetTitle,
this.languageCode,
});
final String fiatAdditionalBalance;
final String fiatAvailableBalance;
final String fiatFrozenBalance;
final Balance raw;
final String fiatAvailableBalanceRaw;
final String fiatAdditionalBalanceRaw;
final String fiatFrozenBalanceRaw;
final String fiatSecondAvailableBalanceRaw;
final String fiatSecondAdditionalBalanceRaw;
final String additionalBalance;
final String availableBalance;
final String frozenBalance;
final String secondAvailableBalance;
final String secondAdditionalBalance;
final String fiatSecondAdditionalBalance;
final String fiatSecondAvailableBalance;
final CryptoCurrency asset;
final CryptoCurrency secondAsset;
final FiatCurrency? fiatCurrency;
final String formattedAssetTitle;
final String? languageCode;
String get combinedAvailableBalance =>
((double.tryParse(availableBalance) ?? 0) + (double.tryParse(secondAvailableBalance) ?? 0))
(raw.available + (raw.secondAvailable ?? Money.zero(raw.available.currency)))
.toString()
.withMaxDecimals(8);
String get combinedFiatAvailableBalance => ((double.tryParse(fiatAvailableBalanceRaw) ?? 0) +
(double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0))
.toStringAsFixed(2);
String get combinedFiatAvailableBalance => fiatCurrency != null
? "$fiatCurrency " +
_withLocalSeperator(((double.tryParse(fiatAvailableBalanceRaw) ?? 0) +
(double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0))
.toStringAsFixed(2))
: "";
String get fiatAvailableBalanceRaw => fiatAvailableBalance.split(" ").last;
String get fiatSecondAvailableBalanceRaw => fiatSecondAvailableBalance.split(" ").last;
String get fiatCurrencyTicker => fiatAvailableBalance.split(" ").first;
String get fiatAvailableBalance =>
fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAvailableBalanceRaw)}" : "";
String get fiatAdditionalBalance =>
fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAdditionalBalanceRaw)}" : "";
String get fiatFrozenBalance =>
fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatFrozenBalanceRaw)}" : "";
String get fiatSecondAvailableBalance => fiatCurrency != null
? "$fiatCurrency ${_withLocalSeperator(fiatSecondAvailableBalanceRaw)}"
: "";
String get fiatSecondAdditionalBalance => fiatCurrency != null
? "$fiatCurrency ${_withLocalSeperator(fiatSecondAdditionalBalanceRaw)}"
: "";
String _withLocalSeperator(String rawAmount) =>
languageCode == null ? rawAmount : rawAmount.withLocalSeperator(languageCode);
}
class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel;
@@ -125,7 +155,7 @@ abstract class BalanceViewModelBase with Store {
bool get isEVMCompatible => isEVMCompatibleChain(wallet.type);
@computed
bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type);
bool get hasAccounts => wallet.hasAccountsSupport;
@computed
SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy;
@@ -208,13 +238,13 @@ abstract class BalanceViewModelBase with Store {
}
}
String additionalBalance(CryptoCurrency cryptoCurrency) {
Money additionalBalance(CryptoCurrency cryptoCurrency) {
final balance = _currencyBalance(cryptoCurrency);
if (displayMode == BalanceDisplayMode.hiddenBalance || balance.additional == BigInt.zero)
return '0.0';
if (displayMode == BalanceDisplayMode.hiddenBalance || balance.unavailable.isZero)
return Money.zero(cryptoCurrency);
return cryptoCurrency.formatAmount(balance.additional);
return balance.unavailable;
}
@computed
@@ -225,24 +255,26 @@ abstract class BalanceViewModelBase with Store {
if (displayMode == BalanceDisplayMode.hiddenBalance) {
final fiatCurrency = settingsStore.fiatCurrency;
return MapEntry(
key,
BalanceRecord(
availableBalance: '●●●●●●',
additionalBalance: additionalBalance(key),
frozenBalance: '',
secondAvailableBalance: '●●●●●●',
secondAdditionalBalance: '●●●●●●',
fiatAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
fiatAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
fiatFrozenBalance: isFiatDisabled ? '' : '',
fiatSecondAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()}',
fiatSecondAdditionalBalance:
isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
asset: key,
secondAsset: secondAsset,
formattedAssetTitle: _formatterAsset(key)));
key,
BalanceRecord(
raw: value,
availableBalance: '●●●●●●',
additionalBalance: '',
frozenBalance: '',
secondAvailableBalance: '●●●●●●',
secondAdditionalBalance: '●●●●●●',
fiatAdditionalBalanceRaw: '●●●●●',
fiatAvailableBalanceRaw: '●●●●●',
fiatFrozenBalanceRaw: '',
fiatSecondAvailableBalanceRaw: '●●●●●',
fiatSecondAdditionalBalanceRaw: '●●●●●',
asset: key,
secondAsset: secondAsset,
fiatCurrency: isFiatDisabled ? null : fiatCurrency,
formattedAssetTitle: _formatterAsset(key),
),
);
}
final fiatCurrency = settingsStore.fiatCurrency;
final price = key.isPotentialScam
? 0.0
: fiatConversionStore.prices[key == CryptoCurrency.btcln ? CryptoCurrency.btc : key] ?? 0;
@@ -251,51 +283,41 @@ abstract class BalanceViewModelBase with Store {
// throw Exception('Price is null for: $key');
// }
final available = evm?.getERC20AvailableBalance(value) ??
(value.fullAvailableBalance - (value.secondAvailable ?? BigInt.zero));
final additional = evm?.getERC20AvailableBalance(value) ??
(value.additional - (value.secondAdditional ?? BigInt.zero));
final availableFiatBalance =
isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.available);
final availableFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(available))}';
final additionalFiatBalance =
isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.unavailable);
final additionalFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(additional))}';
final frozenFiatBalance =
isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.frozen);
final frozenFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.frozen != null ? key.formatAmount(value.frozen!) : null)}';
final secondAvailableFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAvailable != null ? key.formatAmount(value.secondAvailable!) : null)}';
final secondAvailableFiatBalance =
isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.secondAvailable);
final secondAdditionalFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAdditional != null ? key.formatAmount(value.secondAdditional!) : null)}';
: _getFiatBalance(price: price, cryptoAmount: value.secondUnavailable);
return MapEntry(
key,
BalanceRecord(
availableBalance: _getFormattedCryptoAmount(key, available),
fiatAvailableBalance: availableFiatBalance,
additionalBalance: _getFormattedCryptoAmount(key, additional),
fiatAdditionalBalance: additionalFiatBalance,
frozenBalance: (value.frozen ?? BigInt.zero) > BigInt.zero
? _getFormattedCryptoAmount(key, value.frozen ?? BigInt.zero)
: '',
fiatFrozenBalance: frozenFiatBalance,
secondAvailableBalance:
_getFormattedCryptoAmount(secondAsset, value.secondAvailable ?? BigInt.zero),
fiatSecondAvailableBalance: secondAvailableFiatBalance,
secondAdditionalBalance:
_getFormattedCryptoAmount(secondAsset, value.secondAdditional ?? BigInt.zero),
fiatSecondAdditionalBalance: secondAdditionalFiatBalance,
raw: value,
availableBalance: _getFormattedCryptoAmount(value.available),
fiatAvailableBalanceRaw: availableFiatBalance,
additionalBalance: _getFormattedCryptoAmount(value.unavailable),
fiatAdditionalBalanceRaw: additionalFiatBalance,
frozenBalance: _getFormattedCryptoAmount(value.frozen),
fiatFrozenBalanceRaw: frozenFiatBalance,
secondAvailableBalance: _getFormattedCryptoAmount(value.secondAvailable),
fiatSecondAvailableBalanceRaw: secondAvailableFiatBalance,
secondAdditionalBalance: _getFormattedCryptoAmount(value.secondUnavailable),
fiatSecondAdditionalBalanceRaw: secondAdditionalFiatBalance,
asset: key,
secondAsset: secondAsset,
fiatCurrency: isFiatDisabled ? null : settingsStore.fiatCurrency,
formattedAssetTitle: _formatterAsset(key),
languageCode: settingsStore.languageCode,
),
);
});
@@ -306,7 +328,7 @@ abstract class BalanceViewModelBase with Store {
bool hasAdditionalBalance(CryptoCurrency currency) {
final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type);
final isNotZeroAmount = additionalBalance(currency) != "0.0";
final isNotZeroAmount = !additionalBalance(currency).isZero;
return isWalletTypeActivated && isNotZeroAmount;
}
@@ -314,9 +336,9 @@ abstract class BalanceViewModelBase with Store {
@computed
bool get hasSecondAdditionalBalance {
if (wallet.type == WalletType.litecoin && mwebEnabled) {
return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0;
return (wallet.balance[CryptoCurrency.ltc]?.secondUnavailable ?? 0) != 0;
} else if (wallet.type == WalletType.bitcoin) {
return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0;
return (wallet.balance[CryptoCurrency.btc]?.secondUnavailable ?? 0) != 0;
}
return false;
}
@@ -338,12 +360,11 @@ abstract class BalanceViewModelBase with Store {
WalletType.wownero,
].contains(type);
String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, BigInt? amount) {
String _getFormattedCryptoAmount(Money? amount) {
if (amount == null) return "";
return appStore.amountParsingProxy
.getDisplayCryptoStringFromBigInt(amount, cryptoCurrency)
.withMaxDecimals(8)
.asDisplayString(amount)
.withLocalSeperator(settingsStore.languageCode);
}
@@ -390,9 +411,9 @@ abstract class BalanceViewModelBase with Store {
switch (sortBalanceBy) {
case SortBalanceBy.FiatBalance:
final aFiatBalance = _getFiatBalance(
price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance);
price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.raw.available);
final bFiatBalance = _getFiatBalance(
price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance);
price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.raw.available);
return (double.tryParse(bFiatBalance) ?? 0)
.compareTo((double.tryParse(aFiatBalance)) ?? 0);
@@ -416,10 +437,10 @@ abstract class BalanceViewModelBase with Store {
return formattedBalances.firstWhereOrNull((item) =>
(getTokenAddressBasedOnWallet(item.asset) ==
wallet.walletInfo.favoriteTokenAddress)) ??
formattedBalances.elementAt(0);
formattedBalances.elementAtOrNull(0);
}
return formattedBalances.elementAt(0);
return formattedBalances.elementAtOrNull(0);
}
String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
@@ -447,15 +468,13 @@ abstract class BalanceViewModelBase with Store {
double ret = 0.0;
for (final curr in wallet.balance.keys) {
final record = wallet.balance[curr]!;
final available = evm?.getERC20AvailableBalance(record) ??
(record.fullAvailableBalance - (record.secondAvailable ?? BigInt.zero));
final available = record.available - (record.secondAvailable ?? Money.zero(curr));
final price = fiatConversionStore.prices[curr] ?? 0;
ret += double.tryParse(calculateFiatAmount(
price: price, cryptoAmount: curr.formatAmount(available).replaceAll(",", ""))
ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString())
.replaceAll(",", "")) ??
0;
}
return ret.toStringAsFixed(2);
return ret.toStringAsFixed(2).withLocalSeperator(settingsStore.languageCode);
}
Balance _currencyBalance(CryptoCurrency cryptoCurrency) {
@@ -499,13 +518,27 @@ abstract class BalanceViewModelBase with Store {
}
}
String _getFiatBalance({required double price, String? cryptoAmount}) {
if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) {
return '0.00'.withLocalSeperator(settingsStore.languageCode);
String? accountFiatBalance(AccountListItem account) {
if (isFiatDisabled) {
return null;
}
return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount)
.withLocalSeperator(settingsStore.languageCode);
final fiat = settingsStore.fiatCurrency.title;
final value = savedDisplayMode == BalanceDisplayMode.hiddenBalance
? "●●●●●"
: calculateFiatAmount(
price: price,
cryptoAmount: account.balance.toString(),
).withLocalSeperator(settingsStore.languageCode);
return "$value $fiat";
}
String _getFiatBalance({required double price, Money? cryptoAmount}) {
if (cryptoAmount == null) {
return '0.00';
}
return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount.toString(), raw: true);
}
String _formatterAsset(CryptoCurrency asset) {
+345 -142
View File
@@ -5,6 +5,11 @@ 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:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:hash_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cw_core/account.dart';
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
import "package:cw_core/balance_card_layout.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';
@@ -18,7 +23,6 @@ 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';
@@ -35,7 +39,6 @@ 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';
@@ -54,7 +57,7 @@ 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/encryption_file_utils.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
@@ -77,6 +80,7 @@ class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
abstract class DashboardViewModelBase with Store {
DashboardViewModelBase(
{required this.balanceViewModel,
required this.accountListViewModelFactory,
required this.tradeMonitor,
required this.appStore,
required this.tradesStore,
@@ -84,7 +88,6 @@ abstract class DashboardViewModelBase with Store {
required this.orderFilterStore,
required this.transactionFilterStore,
required this.settingsStore,
required this.yatStore,
required this.ordersStore,
required this.anonpayTransactionsStore,
required this.payjoinTransactionsStore,
@@ -95,49 +98,8 @@ abstract class DashboardViewModelBase with Store {
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 (Cake Pay) section removed — Hash Bags does not integrate
// Cake Pay. MoonPay buy orders are source:buy and have no filter row.
S.current.trades: [
FilterItem(
value: () => tradeFilterStore.displayAllTrades,
caption: S.current.all_trades,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
// Only Trocador is enabled as a swap provider (see
// exchange_view_model.dart _allProviders); other providers are
// disabled pending affiliate keys, so they are omitted here too.
FilterItem(
value: () => tradeFilterStore.displayTrocador,
caption: ExchangeProviderDescription.trocador.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
]
},
filterItems = [],
exchangeFilterItems = [],
subname = '',
name = appStore.wallet!.name,
type = appStore.wallet!.type,
@@ -146,6 +108,9 @@ abstract class DashboardViewModelBase with Store {
cardOrder = ObservableMap<int, int>(),
wallet = appStore.wallet! {
showDecredInfoCard = false;
showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
isMultiAccountsEnabled =
wallet.hasNativeAccounts || wallet.walletInfo.isMultiAccountsEnabled == true;
name = wallet.name;
type = wallet.type;
@@ -155,13 +120,15 @@ abstract class DashboardViewModelBase with Store {
unawaited(isBackgroundSyncEnabled());
unawaited(isBatteryOptimizationEnabled());
unawaited(_loadConstraints());
accountListViewModel = accountListViewModelFactory();
final _wallet = wallet;
loadFilterItems();
if (_wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
_onAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction(
@@ -189,9 +156,9 @@ abstract class DashboardViewModelBase with Store {
} else if (_wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction(
_onAccountChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
(wow.Account account) => _onMoneroAccountChange(_wallet));
(Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
@@ -217,21 +184,12 @@ abstract class DashboardViewModelBase with Store {
),
);
} 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'),
),
),
);
subname = "";
_reloadTransactions();
}
_setupBitcoinAccountChangeReaction(_wallet);
// TODO: nano sub-account generation is disabled:
// if (_wallet.type == WalletType.nano || _wallet.type == WalletType.banano) {
// subname = nano!.getCurrentAccount(_wallet).label;
@@ -239,10 +197,13 @@ abstract class DashboardViewModelBase with Store {
_walletChangeDisposer?.reaction.dispose();
_walletChangeDisposer = reaction((_) => appStore.wallet, (wallet) {
accountListViewModel = accountListViewModelFactory();
_onWalletChange(wallet);
resetLightningMode();
_checkMweb();
loadCardDesigns();
showDecredInfoCard = false;
loadSeedBackupReminder();
tradeMonitor.stopTradeMonitoring();
tradeMonitor.monitorActiveTrades(wallet!.id);
@@ -282,6 +243,42 @@ abstract class DashboardViewModelBase with Store {
tradeMonitor.monitorActiveTrades(wallet.id);
}
void loadFilterItems() {
filterItems = [
// FilterItem(
// value: () => transactionFilterStore.displayAll,
// caption: S.current.all_transactions,
// onChanged: transactionFilterStore.toggleAll),
FilterItem(
value: () => transactionFilterStore.displayOutgoing,
caption: S.current.send,
onChanged: transactionFilterStore.toggleOutgoing),
FilterItem(
value: () => transactionFilterStore.displayIncoming,
caption: S.current.receive,
onChanged: transactionFilterStore.toggleIncoming),
if (appStore.wallet!.type == WalletType.bitcoin)
FilterItem(
value: () => transactionFilterStore.displaySilentPayments,
caption: S.current.silent_payments,
onChanged: transactionFilterStore.toggleSilentPayments,
),
SwapFilterItem(
enabledProviders: () => tradeFilterStore.enabledProvidersCount,
allEnabled: () => tradeFilterStore.displayAllTrades,
value: () => tradeFilterStore.enabledProvidersCount > 0,
onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
];
// Hash Bags: Trocador is the only swap provider.
exchangeFilterItems = [
SwapProviderFilterItem(
providerDescription: ExchangeProviderDescription.trocador,
value: () => tradeFilterStore.displayTrocador,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
];
}
bool _isTransactionDisposerCallbackRunning = false;
@action
@@ -292,8 +289,14 @@ abstract class DashboardViewModelBase with Store {
transactions.clear();
final filteredTransactions = wallet.type == WalletType.bitcoin
? bitcoin!.getCurrentAccountBitcoinTransactions(wallet)
: wallet.transactionHistory.transactions.values.toList();
filteredTransactions.sort((a, b) => a.date.compareTo(b.date));
transactions.addAll(
wallet.transactionHistory.transactions.values.map(
filteredTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
@@ -304,6 +307,10 @@ abstract class DashboardViewModelBase with Store {
);
}
@computed
// Hash Bags: no Zcash, so never migrating.
bool get isMigratingToIronwood => false;
@computed
bool get isSyncHeavy {
if ([
@@ -333,64 +340,99 @@ abstract class DashboardViewModelBase with Store {
}
@action
Future<void> loadCardDesigns() async {
final accountStyleSettings =
await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
void changeAllFilterItems(bool value) {
for (final item in filterItems) {
if (item.value() != value) {
item.onChanged();
}
}
for (final item in exchangeFilterItems) {
if (item.value() != value) {
item.onChanged();
}
}
}
late final int numAccounts;
List<int> get cardAccountIndices {
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;
return monero!.getAccountList(wallet).accounts.map((account) => account.id).toList();
}
cardDesigns.clear();
Map<int, int> newOrder = {};
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getAccountList(wallet).accounts.map((account) => account.id).toList();
}
if (wallet.type == WalletType.bitcoin) {
final accounts = accountListViewModel?.accounts;
return [
if (accounts == null || accounts.isEmpty) 0 else ...accounts.map((account) => account.id),
-2,
];
}
return const [-1];
}
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;
@action
Future<void> loadCardDesigns() async {
if (wallet.type == WalletType.bitcoin) {
await accountListViewModel?.reload();
}
final accountIndices = cardAccountIndices;
final settings = await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
if (wallet.type != WalletType.bitcoin && accountIndices.contains(0) &&
!settings.any((setting) => setting.accountIndex == 0)) {
final primarySettings = settings.where((setting) => setting.accountIndex == -1)
.firstOrNull?.copyWith(accountIndex: 0);
if (primarySettings != null) {
await primarySettings.insert();
settings.add(primarySettings);
}
}
final layout = BalanceCardLayout.resolve(
accountIndices: accountIndices.where((index) => index != -2).toList(),
settings: settings,
);
// 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;
}
cardDesigns
..clear()
..addAll(accountIndices.map((accountIndex) => CardDesign.fromStyleSettings(
settings.where((setting) => setting.accountIndex == accountIndex).firstOrNull,
accountIndex == -2 ? CryptoCurrency.btcln : wallet.currency,
)));
cardOrder = {
for (int position = 0; position < layout.visible.length; position++)
position: layout.visible[position],
}.asObservable();
if (layout.needsRepair) {
await BalanceCardStyleSettings.setVisibleOrder(wallet.walletInfo.internalId, layout.orders);
}
if (isMultiAccountsEnabled && !lightningMode && layout.visible.isNotEmpty &&
!layout.visible.contains(accountListViewModel?.selectedAccount?.id)) {
final visibleAccount = accountListViewModel?.accounts
.where((account) => account.id == layout.visible.last).firstOrNull;
if (visibleAccount != null) {
await accountListViewModel!.select(visibleAccount);
}
}
cardOrder = newOrder.asObservable();
}
int get currentCardAccountIndex {
if (wallet.type == WalletType.monero) {
return monero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.bitcoin) {
return lightningMode ? -2 : wallet.walletInfo.currentAccountIndex;
}
return -1;
}
CardDesign get currentCardDesign {
final designIndex = cardAccountIndices.indexOf(currentCardAccountIndex);
return cardDesigns.isEmpty || designIndex >= cardDesigns.length
? CardDesign.fromStyleSettings(null, wallet.currency)
: cardDesigns[designIndex == -1 ? 0 : designIndex];
}
void _transactionDisposerCallback(int _) async {
@@ -405,36 +447,43 @@ abstract class DashboardViewModelBase with Store {
: 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;
}
final allTxs = appStore.wallet!.transactionHistory.transactions.values;
final List<TransactionInfo> relevantTxs;
if (isRelevant) {
relevantTxs.add(tx);
}
if (wallet.type == WalletType.monero) {
relevantTxs = allTxs
.where((tx) => monero!.getTransactionInfoAccountId(tx) == currentAccountId)
.toList();
} else if (wallet.type == WalletType.wownero) {
relevantTxs = allTxs
.where((tx) => wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId)
.toList();
} else if (wallet.type == WalletType.bitcoin) {
relevantTxs = bitcoin!.getCurrentAccountBitcoinTransactions(wallet);
} else {
relevantTxs = allTxs.toList();
}
// printV("Transaction disposer callback (relevantTxs: ${relevantTxs.length} current: ${transactions.length})");
// TODO(malik) update this in a saner way during the vm refactor
String _txIdentityString(String txHash, TransactionDirection direction) =>
"${txHash}_$direction";
String _txIdentityStringConfirmations(
String txHash, TransactionDirection direction, int confirmations) =>
"${txHash}_${direction}_$confirmations";
String txHash, TransactionDirection direction, int confirmations, bool isPending) =>
"${txHash}_${direction}_${confirmations}_$isPending";
final existingKeys = transactions
.map((item) => _txIdentityStringConfirmations(
item.transaction.txHash, item.transaction.direction, item.transaction.confirmations))
item.transaction.txHash,
item.transaction.direction,
item.transaction.confirmations,
item.transaction.isPending))
.toSet();
final newTransactions = relevantTxs
.where((tx) => !existingKeys
.contains(_txIdentityStringConfirmations(tx.txHash, tx.direction, tx.confirmations)))
.where((tx) => !existingKeys.contains(_txIdentityStringConfirmations(
tx.txHash, tx.direction, tx.confirmations, tx.isPending)))
.map((tx) => TransactionListItem(
transaction: tx,
balanceViewModel: balanceViewModel,
@@ -499,6 +548,12 @@ abstract class DashboardViewModelBase with Store {
@observable
ObservableMap<int, int> cardOrder;
@observable
bool lightningMode = false;
@observable
WalletAccountListViewModel? accountListViewModel;
@computed
bool get isDarkTheme => appStore.themeStore.currentTheme.isDark;
@@ -513,13 +568,14 @@ abstract class DashboardViewModelBase with Store {
@computed
bool get shouldShowMwebAd {
return false;
if (wallet.type != WalletType.litecoin) return false;
if (mwebEnabled) return false;
if (settingsStore.mwebAdDismissed) return false;
return Platform.isAndroid || Platform.isIOS;
return (Platform.isAndroid || Platform.isIOS) && !wallet.isHardwareWallet;
}
@action
@@ -645,6 +701,12 @@ abstract class DashboardViewModelBase with Store {
return formattedItemsList(_items);
}
static const shortHistoryLength = 3;
@computed
List<ActionListItem> get itemsShort =>
items.where((item) => item is! DateSectionItem).take(shortHistoryLength).toList();
@observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
@@ -787,6 +849,60 @@ abstract class DashboardViewModelBase with Store {
@observable
late bool showDecredInfoCard;
@observable
late bool showSeedBackupReminder;
@computed
bool get canToggleMultiAccounts => wallet.canToggleMultiAccounts;
@observable
late bool isMultiAccountsEnabled;
@computed
List<AccountListItem> get visibleAccounts {
final all = accountListViewModel?.accounts;
if (all == null || all.isEmpty) return const <AccountListItem>[];
if (isMultiAccountsEnabled) {
return all.where((account) => cardOrder.containsValue(account.id)).toList(growable: false);
}
final primary = all.where((a) => a.id == 0).firstOrNull ?? all.first;
return <AccountListItem>[primary];
}
@action
Future<void> setMultiAccountsEnabled(bool value) async {
if (!canToggleMultiAccounts) {
return;
}
if (!value) {
await _selectPrimaryAccount();
}
wallet.walletInfo.isMultiAccountsEnabled = value;
await wallet.walletInfo.save();
isMultiAccountsEnabled = value;
await accountListViewModel?.reload();
await loadCardDesigns();
if (value) {
unawaited(wallet.startSync());
}
}
@computed
bool get hasBalance => wallet.balance.values.any(
(balance) =>
!balance.available.isZero ||
!balance.unavailable.isZero ||
!(balance.secondAvailable?.isZero ?? true) ||
!(balance.secondUnavailable?.isZero ?? true) ||
!(balance.frozen?.isZero ?? true),
);
@computed
bool get shouldShowSeedBackupReminder => showSeedBackupReminder && hasBalance;
@computed
bool get showPayjoinCard =>
wallet.type == WalletType.bitcoin &&
@@ -1011,6 +1127,17 @@ abstract class DashboardViewModelBase with Store {
sharedPreferences.setBool(PreferencesKey.showDecredInfoCard, false);
}
@action
void loadSeedBackupReminder() {
showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
}
@action
Future<void> dismissSeedBackupReminder() async {
showSeedBackupReminder = false;
await wallet.walletInfo.clearSeedBackupReminder();
}
@action
void dismissPayjoin() {
settingsStore.showPayjoinCard = false;
@@ -1023,6 +1150,8 @@ abstract class DashboardViewModelBase with Store {
bitcoin!.updatePayjoinState(wallet, true);
}
final WalletAccountListViewModel? Function() accountListViewModelFactory;
BalanceViewModel balanceViewModel;
TradeMonitor tradeMonitor;
@@ -1031,7 +1160,6 @@ abstract class DashboardViewModelBase with Store {
SettingsStore settingsStore;
YatStore yatStore;
TradesStore tradesStore;
@@ -1047,7 +1175,11 @@ abstract class DashboardViewModelBase with Store {
PayjoinTransactionsStore payjoinTransactionsStore;
Map<String, List<FilterItem>> filterItems;
// Map<String, List<FilterItem>> filterItems;
List<FilterItem> filterItems;
List<FilterItem> exchangeFilterItems;
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
@@ -1074,7 +1206,7 @@ abstract class DashboardViewModelBase with Store {
@computed
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
ReactionDisposer? _onMoneroAccountChangeReaction;
ReactionDisposer? _onAccountChangeReaction;
ReactionDisposer? _onMoneroBalanceChangeReaction;
@@ -1142,6 +1274,37 @@ abstract class DashboardViewModelBase with Store {
}
}
@action
void toggleLightningMode() {
lightningMode = !lightningMode;
// Lightning belongs to the primary account only
if (lightningMode) {
unawaited(_selectPrimaryAccount().then((_) async {
if (!lightningMode && isMultiAccountsEnabled) {
await loadCardDesigns();
}
}));
} else {
unawaited(loadCardDesigns());
}
}
Future<void> _selectPrimaryAccount() async {
if (wallet.type != WalletType.bitcoin) return;
final vm = accountListViewModel;
if (vm == null || vm.selectedAccount?.id == 0) return;
final primary = vm.accounts.where((a) => a.id == 0).firstOrNull;
if (primary != null) {
await vm.select(primary);
}
}
@action
void resetLightningMode() => lightningMode = false;
@action
void _onWalletChange(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
@@ -1152,15 +1315,20 @@ abstract class DashboardViewModelBase with Store {
this.wallet = wallet;
type = wallet.type;
name = wallet.name;
isMultiAccountsEnabled =
wallet.hasNativeAccounts || wallet.walletInfo.isMultiAccountsEnabled == true;
_onAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction = null;
loadFilterItems();
if (wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(wallet).label;
_onMoneroAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
_onAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction(
@@ -1171,12 +1339,12 @@ abstract class DashboardViewModelBase with Store {
} else if (wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(wallet).label;
_onMoneroAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction(
_onAccountChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
(wow.Account account) => _onMoneroAccountChange(wallet));
(Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
@@ -1187,7 +1355,7 @@ abstract class DashboardViewModelBase with Store {
// FIX-ME: Check for side effects
// subname = null;
subname = '';
_setupBitcoinAccountChangeReaction(wallet);
_reloadTransactions();
}
@@ -1223,6 +1391,21 @@ abstract class DashboardViewModelBase with Store {
}, _transactionDisposerCallback, delay: 300);
}
void _setupBitcoinAccountChangeReaction(WalletBase wallet) {
if (wallet.type != WalletType.bitcoin) {
return;
}
_onAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction = reaction(
(_) => accountListViewModel?.selectedAccount?.id,
(_) {
_reloadTransactions();
},
fireImmediately: true,
);
}
@action
void _onMoneroAccountChange(WalletBase wallet) {
if (wallet.type == WalletType.monero) {
@@ -1349,7 +1532,8 @@ abstract class DashboardViewModelBase with Store {
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 encryption = encryptionFileUtilsFor(SettingsStoreBase.walletPasswordDirectInput);
final jsonSource = await encryption.read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
@@ -1406,4 +1590,23 @@ abstract class DashboardViewModelBase with Store {
Future<void> refreshDashboard() async {
reconnect();
}
// Hash Bags: Zano never shipped here, nothing to deprecate.
static const walletTypesToBeRemoved = <WalletType>[];
Future<bool> shouldShowRemovalPopup() async {
final show = (await WalletInfo.getAll()).any((item) =>
walletTypesToBeRemoved.contains(item.type) &&
!(sharedPreferences.getBool(PreferencesKey.deprecationPopupViewed(item.type)) ?? false),);
for (final type in walletTypesToBeRemoved) {
await sharedPreferences.setBool(PreferencesKey.deprecationPopupViewed(type), true);
}
// if user is actively using a wallet type that's about to be removed, keep nagging them
return show || walletTypesToBeRemoved.contains(wallet.type);
}
Future<List<WalletInfo>> get walletsToBeRemoved async => (await WalletInfo.getAll())
.where((item) => walletTypesToBeRemoved.contains(item.type))
.toList();
}
@@ -1,3 +1,4 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
class DateSectionItem extends ActionListItem {
@@ -6,3 +7,30 @@ class DateSectionItem extends ActionListItem {
@override
final DateTime date;
}
abstract class SpecificDateSectionItem extends DateSectionItem {
SpecificDateSectionItem(super.date, {required super.key});
String get text;
}
class Last30daysTransactionItem extends SpecificDateSectionItem {
Last30daysTransactionItem(super.date, {required super.key});
@override
String get text => S.current.last_30_days;
}
class Last7daysTransactionItem extends SpecificDateSectionItem {
Last7daysTransactionItem(super.date, {required super.key});
@override
String get text => S.current.last_7_days;
}
class TodayTransactionItem extends SpecificDateSectionItem {
TodayTransactionItem(super.date, {required super.key});
@override
String get text => S.current.today;
}
@@ -1,35 +0,0 @@
import 'package:mobx/mobx.dart';
part 'desktop_sidebar_view_model.g.dart';
enum SidebarItem {
dashboard,
transactions,
status,
support,
settings,
}
class DesktopSidebarViewModel = DesktopSidebarViewModelBase with _$DesktopSidebarViewModel;
abstract class DesktopSidebarViewModelBase with Store {
DesktopSidebarViewModelBase();
@observable
SidebarItem currentPage = SidebarItem.dashboard;
@action
void onPageChange(SidebarItem item) {
if (currentPage == item) {
resetSidebar();
return;
}
currentPage = item;
}
@action
void resetSidebar() {
currentPage = SidebarItem.dashboard;
}
}
@@ -1,19 +0,0 @@
import 'package:hash_wallet/view_model/dashboard/filter_item.dart';
class DropdownFilterItem extends FilterItem {
DropdownFilterItem({
required this.items,
required this.caption,
required this.selectedItem,
required this.onItemSelected,
}) : super(
value: () => false,
caption: caption,
onChanged: (_) {},
);
final List<String> items;
final String caption;
final String selectedItem;
final Function(String) onItemSelected;
}
+27 -1
View File
@@ -1,4 +1,4 @@
import 'package:mobx/mobx.dart';
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
class FilterItem {
FilterItem({required this.value, required this.caption, required this.onChanged});
@@ -7,3 +7,29 @@ class FilterItem {
String caption;
Function onChanged;
}
class SwapFilterItem extends FilterItem {
SwapFilterItem({
required this.enabledProviders,
required this.allEnabled,
required super.value,
super.caption = "Swap",
required super.onChanged,
});
int Function() enabledProviders;
bool Function() allEnabled;
}
class SwapProviderFilterItem extends FilterItem {
SwapProviderFilterItem(
{required super.value,
required super.onChanged,
required this.providerDescription,
super.caption = ""});
@override
String get caption => providerDescription.title;
final ExchangeProviderDescription providerDescription;
}
@@ -1,43 +1,80 @@
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
import 'package:flutter/foundation.dart';
import "package:hash_wallet/view_model/dashboard/action_list_item.dart";
import "package:hash_wallet/view_model/dashboard/date_section_item.dart";
import "package:flutter/foundation.dart";
enum _DateBucket { recent, last7Days, last30Days, byMonth }
List<ActionListItem> formattedItemsList(List<ActionListItem> items) {
final formattedList = <ActionListItem>[];
DateTime? lastDate;
items.sort((a, b) => b.date.compareTo(a.date));
for (var i = 0; i < items.length; i++) {
final transaction = items[i];
final now = DateTime.now();
final todayTreshold = DateTime(now.year, now.month, now.day);
final last7daysThreshold = now.subtract(const Duration(days: 7));
final last30daysThreshold = now.subtract(const Duration(days: 30));
if (lastDate == null) {
lastDate = transaction.date;
formattedList.add(
DateSectionItem(
transaction.date,
key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'),
),
);
formattedList.add(transaction);
continue;
_DateBucket? lastBucket;
DateTime? lastMonthDate;
for (final transaction in items) {
final date = transaction.date;
final bucket = date.isAfter(todayTreshold)
? _DateBucket.recent
: date.isAfter(last7daysThreshold)
? _DateBucket.last7Days
: date.isAfter(last30daysThreshold)
? _DateBucket.last30Days
: _DateBucket.byMonth;
switch (bucket) {
case _DateBucket.recent:
if (lastBucket != _DateBucket.recent) {
formattedList.add(
TodayTransactionItem(
date,
key: const ValueKey("today_section_item_key"),
),
);
}
break;
case _DateBucket.last7Days:
if (lastBucket != _DateBucket.last7Days) {
formattedList.add(
Last7daysTransactionItem(
date,
key: const ValueKey("last_7_days_section_item_key"),
),
);
}
break;
case _DateBucket.last30Days:
if (lastBucket != _DateBucket.last30Days) {
formattedList.add(
Last30daysTransactionItem(
date,
key: const ValueKey("last_30_days_section_item_key"),
),
);
}
break;
case _DateBucket.byMonth:
final isNewMonth = lastMonthDate == null ||
lastMonthDate.year != date.year ||
lastMonthDate.month != date.month;
if (isNewMonth) {
lastMonthDate = date;
formattedList.add(
DateSectionItem(
date,
key: ValueKey("date_section_item_${date.year}_${date.month}_key"),
),
);
}
break;
}
final isCurrentDay = lastDate.year == transaction.date.year &&
lastDate.month == transaction.date.month &&
lastDate.day == transaction.date.day;
if (isCurrentDay) {
formattedList.add(transaction);
continue;
}
lastDate = transaction.date;
formattedList.add(
DateSectionItem(
transaction.date,
key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'),
),
);
lastBucket = bucket;
formattedList.add(transaction);
}
@@ -151,10 +151,10 @@ abstract class HomeSettingsViewModelBase with Store {
return false;
}
bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis(
contractAddress,
getChainNameBasedOnWalletType(_balanceViewModel.wallet.type),
);
final moralisChainName = evm!.getMoralisChainName(_balanceViewModel.wallet);
final isPotentialScamViaMoralis = moralisChainName != null &&
await _isPotentialScamTokenViaMoralis(contractAddress, moralisChainName);
bool isUnverifiedContract = await _isContractUnverified(
contractAddress,
@@ -333,6 +333,10 @@ abstract class HomeSettingsViewModelBase with Store {
return null;
}
Future<bool?> checkIfTokenIsVerifiedOnJupiter(String mintAddress) async {
return null; // Hash Bags: Solana removed
}
CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency;
void _updateFiatPrices(CryptoCurrency token) async {
@@ -0,0 +1,140 @@
import "dart:async";
import "package:hash_wallet/core/execution_state.dart";
import "package:hash_wallet/entities/solana_nft_asset_model.dart";
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:hash_wallet/view_model/contact_list/contact_list_view_model.dart";
import "package:cw_core/exceptions.dart";
import "package:cw_core/pending_transaction.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:mobx/mobx.dart";
part "nft_send_view_model.g.dart";
class NFTSendViewModel = NFTSendViewModelBase with _$NFTSendViewModel;
abstract class NFTSendViewModelBase with Store {
NFTSendViewModelBase(this._appStore, this._contactListViewModel);
final AppStore _appStore;
final ContactListViewModel _contactListViewModel;
@observable
ExecutionState state = InitialExecutionState();
@observable
PendingTransaction? pendingTransaction;
bool shouldRequireTOTP2FAFor(String destinationAddress) {
final settingsStore = _appStore.settingsStore;
final isContact = _contactListViewModel.contactsToShow
.any((contact) => contact.address == destinationAddress);
if (isContact) {
return settingsStore.shouldRequireTOTP2FAForSendsToContact;
}
final isInternalWallet = _contactListViewModel.walletContactsToShow
.any((contact) => contact.address == destinationAddress);
if (isInternalWallet) {
return settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
}
return settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
}
@action
Future<void> createTransaction(SolanaNFTAssetModel asset, String destinationAddress) async {
state = IsExecutingState();
try {
throw UnsupportedError('Solana NFTs are not supported');
state = ExecutedSuccessfullyState();
} catch (e) {
pendingTransaction = null;
state = FailureState(_errorMessage(e));
}
}
@action
Future<void> commitTransaction() async {
state = IsExecutingState();
try {
final wallet = _appStore.wallet!;
final transaction = pendingTransaction!;
await transaction.commit();
final signature = transaction.id;
pendingTransaction = null;
state = ExecutedSuccessfullyState();
if (signature.isNotEmpty) {
unawaited(_pollForTransaction(wallet, signature));
}
} catch (e) {
state = FailureState(_errorMessage(e));
}
}
/// Re-entrancy latch for the send button, held only while the tap handler is
/// inside [AuthService.authenticateAction].
///
/// Deliberately not [state]: the auth sheet is pushed closable, and closing it
/// pops the route without ever invoking onAuthenticationFinished, so a flag
/// that waits on the auth result would never be cleared. The caller clears
/// this in a finally instead. Once auth succeeds, createTransaction takes over
/// the guard by setting IsExecutingState synchronously.
bool _isStartingSend = false;
bool get isStartingSend => _isStartingSend;
void markSendStarted() => _isStartingSend = true;
void markSendSettled() => _isStartingSend = false;
@action
void reset() {
_isStartingSend = false;
pendingTransaction = null;
state = InitialExecutionState();
}
Future<void> _pollForTransaction(WalletBase wallet, String signature) async {
try {
} catch (e) {
printV("Failed to poll for NFT transfer: ${e.toString()}");
}
}
String _errorMessage(Object error) {
if (error is NotAnNFTException) {
return S.current.nft_send_not_an_nft;
}
if (error is NoAssociatedTokenAccountException) {
return S.current.solana_no_associated_token_account_exception;
}
if (error is SignSPLTokenTransactionRentException) {
return S.current.solana_sign_spl_token_transaction_rent_exception;
}
if (error is CreateAssociatedTokenAccountException) {
return "${S.current.solana_create_associated_token_account_exception} "
"${S.current.added_message_for_ata_error}";
}
printV("NFT send failed: ${error.toString()}");
return error.toString();
}
}
+171 -95
View File
@@ -1,12 +1,15 @@
import "dart:async";
import 'dart:convert';
import 'dart:developer';
import "package:hash_wallet/entities/saved_nfts.dart";
import 'package:hash_wallet/entities/solana_nft_asset_model.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:hash_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/utils/debounce.dart';
import "package:cw_core/utils/print_verbose.dart";
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:mobx/mobx.dart';
@@ -31,12 +34,27 @@ abstract class NFTViewModelBase with Store {
if (wallet != null) return wallet.chainId;
return null;
}, (_) => getNFTAssetByWallet());
}, (_) => _fetchDebounce.run(() => getNFTAssetByWallet()));
}
}
final AppStore appStore;
bool _nftErrorPopupShown = false;
final BottomSheetService bottomSheetService;
final Debounce _fetchDebounce = Debounce(const Duration(milliseconds: 500));
Future<void> _lastFetch = Future.value();
Completer<void>? _queuedFetch;
final SavedNFTs _savedNFTs = SavedNFTs();
int _currentRefreshId = 0;
static const _requestTimeout = Duration(seconds: 20);
Map<String, String> get _moralisHeaders => {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
};
@observable
bool isLoading;
@@ -44,137 +62,195 @@ abstract class NFTViewModelBase with Store {
@observable
bool isImportNFTLoading;
@observable
int unresolvedNFTCount = 0;
ObservableList<NFTAssetModel> nftAssetByWalletModels;
ObservableList<SolanaNFTAssetModel> solanaNftAssetModels;
// Hash Bags: Solana is not supported.
bool get isSolanaWallet => false;
String? get currentWalletName => appStore.wallet?.name;
@action
Future<void> getNFTAssetByWallet() async {
final wallet = appStore.wallet!;
void onWalletChanged() {
nftAssetByWalletModels.clear();
solanaNftAssetModels.clear();
unresolvedNFTCount = 0;
_nftErrorPopupShown = false;
_fetchDebounce.run(getNFTAssetByWallet);
}
if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) return;
@action
Future<void> getNFTAssetByWallet() {
final queued = _queuedFetch;
final walletAddress = wallet.walletInfo.address;
log('Fetching wallet NFTs for $walletAddress');
if (queued != null) {
return queued.future;
}
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: wallet.chainId);
// the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet
// the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response.
final done = Completer<void>();
_queuedFetch = done;
Uri uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/$walletAddress/nft',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"exclude_spam": "true",
"normalizeMetadata": "true",
},
);
final waitFor = _lastFetch;
_lastFetch = done.future;
unawaited(_fetchAfterPreviousRun(waitFor, done));
return done.future;
}
Future<void> _fetchAfterPreviousRun(Future<void> waitFor, Completer<void> done) async {
await waitFor;
if (identical(_queuedFetch, done)) {
_queuedFetch = null;
}
try {
if (isLoading) return;
final wallet = appStore.wallet!;
if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) {
return;
}
final walletAddress = wallet.walletInfo.address;
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: wallet.chainId);
isLoading = true;
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final refreshId = ++_currentRefreshId;
final decodedResponse = jsonDecode(response.body);
await _getEvmNFTAssets(wallet.name, walletAddress, chainName, refreshId);
final result =
WalletNFTsResponseModel.fromJson(decodedResponse as Map<String, dynamic>).result ?? [];
nftAssetByWalletModels.clear();
nftAssetByWalletModels.addAll(result);
_nftErrorPopupShown = false;
} catch (e) {
log(e.toString());
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.moralis_nft_error,
),
);
printV("Fetching wallet NFTs failed: ${e.toString()}");
if (!_nftErrorPopupShown) {
_nftErrorPopupShown = true;
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.moralis_nft_error,
),
);
}
} finally {
isLoading = false;
done.complete();
}
}
Future<SolanaNFTAssetModel> getSolanaNFTDetails(String address, String chainName) async {
final uri = Uri.https(
'solana-gateway.moralis.io',
'/nft/$chainName/$address/metadata',
);
Future<void> _getEvmNFTAssets(
String walletName,
String walletAddress,
String chainName,
int refreshId,
) async {
final saved = await _savedNFTs.evmNFTs(walletName, chainName);
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
var result = const <NFTAssetModel>[];
Object? indexerError;
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
try {
final response = await ProxyWrapper()
.get(
clearnetUri: Uri.https(
"deep-index.moralis.io",
"/api/v2.2/$walletAddress/nft",
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"exclude_spam": "true",
"normalizeMetadata": "true",
},
),
headers: _moralisHeaders,
)
.timeout(_requestTimeout);
return SolanaNFTAssetModel.fromJson(decodedResponse);
result = WalletNFTsResponseModel.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
).result ??
[];
} catch (e) {
indexerError = e;
}
if (appStore.wallet?.walletInfo.address != walletAddress || refreshId != _currentRefreshId) {
return;
}
final listed = result.map(SavedNFTs.evmIdentifier).toSet();
nftAssetByWalletModels.clear();
nftAssetByWalletModels.addAll(result);
nftAssetByWalletModels
.addAll(saved.where((asset) => !listed.contains(SavedNFTs.evmIdentifier(asset))));
if (indexerError != null) {
throw indexerError;
}
}
@action
Future<void> onNFTSent(String walletName, String mint) async {
await _savedNFTs.removeSolana(walletName, mint);
if (appStore.wallet?.name == walletName) {
solanaNftAssetModels.removeWhere((asset) => asset.mint == mint);
}
}
Future<void> _importEvmNFT(
String walletName,
String tokenAddress,
String? tokenId,
String chainName,
) async {
final response = await ProxyWrapper()
.get(
clearnetUri: Uri.https(
"deep-index.moralis.io",
"/api/v2.2/nft/$tokenAddress/$tokenId",
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"normalizeMetadata": "true",
},
),
headers: _moralisHeaders,
)
.timeout(_requestTimeout);
final asset = NFTAssetModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
nftAssetByWalletModels.add(asset);
await _savedNFTs.addEvm(walletName, chainName, asset);
}
@action
Future<void> importNFT(String tokenAddress, String? tokenId) async {
final walletType = appStore.wallet!.type;
final wallet = appStore.wallet!;
int? chainId;
if (isEVMCompatibleChain(walletType)) {
chainId = evm!.getSelectedChainId(appStore.wallet!);
if (isEVMCompatibleChain(wallet.type)) {
chainId = evm!.getSelectedChainId(wallet);
}
final chainName = getChainNameBasedOnWalletType(walletType, chainId: chainId);
// the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: chainId);
try {
isImportNFTLoading = true;
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/nft/$tokenAddress/$tokenId',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"normalizeMetadata": "true",
},
);
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
final nftAsset = NFTAssetModel.fromJson(decodedResponse);
nftAssetByWalletModels.add(nftAsset);
await _importEvmNFT(wallet.name, tokenAddress, tokenId, chainName);
} catch (e) {
printV("Importing NFT $tokenAddress failed: ${e.toString()}");
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: e.toString(),
message: S.current.nft_import_failed,
),
);
} finally {
@@ -22,7 +22,9 @@ abstract class ReceiveOptionViewModelBase with Store {
@observable
ReceivePageOption selectedReceiveOption;
List<ReceivePageOption> get options => _wallet.walletAddresses.receivePageOptions;
@computed
List<ReceivePageOption> get options =>
_wallet.walletAddresses.receivePageOptions.where(_wallet.receiveOptionAvailable).toList();
String get walletTypeString => walletTypeToString(_wallet.type);
@@ -2,17 +2,13 @@ import 'package:hash_wallet/entities/balance_display_mode.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/nano/nano.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount_raw.dart';
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/keyable.dart';
@@ -44,22 +40,26 @@ class TransactionListItem extends ActionListItem with Keyable {
String get formattedCryptoAmount {
if (displayMode == BalanceDisplayMode.hiddenBalance) return '---';
if (balanceViewModel.wallet.type == WalletType.bitcoin) {
final isLightning = (transaction.additionalInfo["isLightning"] as bool?) ?? false;
final crypto = isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc;
final amount = _appStore.amountParsingProxy
.getDisplayCryptoString(transaction.amount, crypto)
.withMaxDecimals(8)
return _appStore.amountParsingProxy
.asDisplayStringWithSymbol(transaction.amount)
.withLocalSeperator(_appStore.settingsStore.languageCode);
return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}';
}
return transaction.amountFormatted();
return transaction.amount.toStringWithSymbol(fractionalDigits: 8);
}
String get formattedTitle {
if (transaction.additionalInfo['autoShield'] == true) {
return "Autoshield";
if (transaction.additionalInfo['isIronwoodMigration'] == true) {
return 'Migration';
}
if (transaction.additionalInfo['isAutoShield'] == true) {
if (transaction.isPending) {
final status = formattedStatus;
final baseString = S.current.shielding;
return status.isNotEmpty ? "$baseString $status" : "$baseString...";
}
return S.current.shielding;
}
if (transaction.isPending) {
final status = formattedStatus;
@@ -168,42 +168,25 @@ class TransactionListItem extends ActionListItem with Keyable {
switch (balanceViewModel.wallet.type) {
case WalletType.monero:
amount = calculateFiatAmountRaw(
cryptoAmount: monero!.formatterMoneroAmountToDouble(amount: transaction.amount),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.wownero:
amount = calculateFiatAmountRaw(
cryptoAmount: wownero!.formatterWowneroAmountToDouble(amount: transaction.amount),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.bitcoinCash:
case WalletType.dogecoin:
case WalletType.nano:
amount = calculateFiatAmountRaw(
cryptoAmount: bitcoin!.formatterBitcoinAmountToDouble(amount: transaction.amount),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
final asset = evm!.assetOfTransaction(balanceViewModel.wallet, transaction);
final asset = assetOfTransaction;
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: evm!.formatterEVMAmountToDouble(transaction: transaction),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.nano:
amount = calculateFiatAmountRaw(
cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString(
nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
@@ -1,6 +0,0 @@
class WalletBalance {
WalletBalance({required this.unlockedBalance, required this.totalBalance});
String unlockedBalance;
String totalBalance;
}