mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-30 06:48:49 +01:00
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:
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user