mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-18 05:51:34 +02:00
94 files changed, -5926 net lines. No stubs, no generators, no dead enum
values, no orphan switch branches. Removed for principled reasons (we
do not ship these chains) and to make the codebase smaller and friendlier
to contribute to.
What's gone:
- lib/{solana,tron,zano,decred,zcash,haven}/ directories
- lib/exchange/provider/jupiter_exchange_provider.dart (Solana-only)
- lib/entities/haven_seed_store.dart
- WalletType.{solana,tron,zano,decred,zcash,haven} enum values
(HiveField IDs are pinned: Wownero stays at 12, all surviving types
keep their IDs, so existing user-saved wallets deserialize unchanged)
- All ~70 lib/ files that imported the removed chain stubs or branched
on the removed WalletType values
- backupSeeds(Box<HavenSeedStore>) abstract method + Wownero implementation
- Jupiter exchange branches in send/exchange view models
- TronGrid usage settings, Zano alias lookups, Zcash transparent address
handling, Decred default-spv-nodes branches, Haven seed migration
What stays (intentional):
- CryptoCurrency.{xhv,sol,trx,dcr,zano,zec} enum entries in cw_core —
they're still used as exchange/swap currency identifiers in
address_validator.dart and elsewhere. The wallet types are gone but
the *currencies* are still tradeable through Trocador.
- cw_core/spl_token.dart and cw_core/tron_token.dart Hive adapters in
main.dart — the tokens still register as Hive types for backwards-
compat with any old box data.
- lib/buy/{moonpay,robinhood,onramper,dfx,kryptonim,meld}/ provider
source files (just stripped of solana/tron/etc references; providers
themselves remain dormant per provider_types.dart).
Pinned-ID note: WalletType enum now has gaps in @HiveField IDs (4, 10,
11, 13, 14, 18 removed). This is deliberate — re-using a freed ID later
would mis-deserialize old hive boxes. Don't renumber.
Bundled config / CI hasn't changed; configure.dart was already free of
these chains in commit 4eb7db0b7.
510 lines
17 KiB
Dart
510 lines
17 KiB
Dart
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
|
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/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:cw_core/crypto_amount_format.dart';
|
|
import 'package:cw_core/transaction_history.dart';
|
|
import 'package:cw_core/wallet_base.dart';
|
|
import 'package:hash_wallet/store/app_store.dart';
|
|
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
|
|
import 'package:hash_wallet/store/settings_store.dart';
|
|
import 'package:cw_core/balance.dart';
|
|
import 'package:cw_core/crypto_currency.dart';
|
|
import 'package:cw_core/erc20_token.dart';
|
|
import 'package:cw_core/spl_token.dart';
|
|
import 'package:cw_core/transaction_info.dart';
|
|
import 'package:cw_core/wallet_type.dart';
|
|
import 'package:mobx/mobx.dart';
|
|
|
|
part 'balance_view_model.g.dart';
|
|
|
|
class BalanceRecord {
|
|
const BalanceRecord({
|
|
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.asset,
|
|
required this.secondAsset,
|
|
required this.formattedAssetTitle,
|
|
});
|
|
|
|
final String fiatAdditionalBalance;
|
|
final String fiatAvailableBalance;
|
|
final String fiatFrozenBalance;
|
|
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 String formattedAssetTitle;
|
|
|
|
String get combinedAvailableBalance =>
|
|
((double.tryParse(availableBalance) ?? 0) + (double.tryParse(secondAvailableBalance) ?? 0))
|
|
.toString().withMaxDecimals(8);
|
|
|
|
String get combinedFiatAvailableBalance =>
|
|
((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;
|
|
}
|
|
|
|
class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel;
|
|
|
|
abstract class BalanceViewModelBase with Store {
|
|
BalanceViewModelBase(
|
|
{required this.appStore, required this.settingsStore, required this.fiatConversionStore})
|
|
: isReversing = false,
|
|
isShowCard = appStore.wallet?.walletInfo.isShowIntroCakePayCard ?? false,
|
|
wallet = appStore.wallet! {
|
|
reaction((_) => appStore.wallet, (wallet) {
|
|
_onWalletChange(wallet);
|
|
_checkMweb();
|
|
});
|
|
|
|
_checkMweb();
|
|
|
|
reaction((_) => settingsStore.mwebAlwaysScan, (_) => _checkMweb());
|
|
}
|
|
|
|
void _checkMweb() {
|
|
if (wallet.type == WalletType.litecoin) {
|
|
mwebEnabled = bitcoin!.getMwebEnabled(wallet);
|
|
}
|
|
}
|
|
|
|
final AppStore appStore;
|
|
final SettingsStore settingsStore;
|
|
final FiatConversionStore fiatConversionStore;
|
|
|
|
@observable
|
|
bool isReversing;
|
|
|
|
@observable
|
|
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
|
|
|
|
@computed
|
|
double get price {
|
|
final price = fiatConversionStore.prices[appStore.wallet!.currency];
|
|
|
|
// price should update on next fetch:
|
|
if (price == null) return 0;
|
|
|
|
return price;
|
|
}
|
|
|
|
@computed
|
|
BalanceDisplayMode get savedDisplayMode => settingsStore.balanceDisplayMode;
|
|
|
|
@computed
|
|
bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled;
|
|
|
|
@computed
|
|
bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type);
|
|
|
|
@computed
|
|
bool get isEVMCompatible => isEVMCompatibleChain(wallet.type);
|
|
|
|
@computed
|
|
bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type);
|
|
|
|
@computed
|
|
SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy;
|
|
|
|
@computed
|
|
bool get pinNativeToken => settingsStore.pinNativeTokenAtTop;
|
|
|
|
@computed
|
|
String get asset {
|
|
if (isEVMCompatibleChain(wallet.type)) {
|
|
final currentChain = evm!.getCurrentChain(wallet);
|
|
if (currentChain != null) {
|
|
return currentChain.name;
|
|
}
|
|
|
|
return walletTypeToString(wallet.type);
|
|
}
|
|
|
|
final typeFormatted = walletTypeToString(wallet.type);
|
|
|
|
return typeFormatted;
|
|
}
|
|
|
|
@computed
|
|
BalanceDisplayMode get displayMode {
|
|
if (isReversing) {
|
|
if (savedDisplayMode == BalanceDisplayMode.hiddenBalance) {
|
|
return BalanceDisplayMode.displayableBalance;
|
|
} else {
|
|
return BalanceDisplayMode.hiddenBalance;
|
|
}
|
|
}
|
|
|
|
return savedDisplayMode;
|
|
}
|
|
|
|
@computed
|
|
String get availableBalanceLabel {
|
|
if (displayMode == BalanceDisplayMode.hiddenBalance) {
|
|
return S.current.show_balance;
|
|
} else {
|
|
return S.current.xmr_available_balance;
|
|
}
|
|
}
|
|
|
|
@computed
|
|
String get additionalBalanceLabel {
|
|
switch (wallet.type) {
|
|
case WalletType.ethereum:
|
|
case WalletType.polygon:
|
|
case WalletType.base:
|
|
case WalletType.arbitrum:
|
|
case WalletType.bsc:
|
|
return S.current.xmr_full_balance;
|
|
case WalletType.nano:
|
|
case WalletType.banano:
|
|
return S.current.receivable_balance;
|
|
default:
|
|
return S.current.unconfirmed;
|
|
}
|
|
}
|
|
|
|
@computed
|
|
String get secondAvailableBalanceLabel {
|
|
switch (wallet.type) {
|
|
case WalletType.litecoin:
|
|
return S.current.mweb_confirmed;
|
|
default:
|
|
return S.current.confirmed;
|
|
}
|
|
}
|
|
|
|
@computed
|
|
String get secondAdditionalBalanceLabel {
|
|
switch (wallet.type) {
|
|
case WalletType.litecoin:
|
|
return S.current.mweb_unconfirmed;
|
|
default:
|
|
return S.current.unconfirmed;
|
|
}
|
|
}
|
|
|
|
String additionalBalance(CryptoCurrency cryptoCurrency) {
|
|
final balance = _currencyBalance(cryptoCurrency);
|
|
|
|
if (displayMode == BalanceDisplayMode.hiddenBalance || balance.additional == BigInt.zero) return '0.0';
|
|
|
|
return cryptoCurrency.formatAmount(balance.additional);
|
|
}
|
|
|
|
@computed
|
|
Map<CryptoCurrency, BalanceRecord> get balances {
|
|
return wallet.balance.map((key, value) {
|
|
var secondAsset = key == CryptoCurrency.ltc ? CryptoCurrency.ltcmweb : key;
|
|
|
|
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)));
|
|
}
|
|
final fiatCurrency = settingsStore.fiatCurrency;
|
|
final price = key.isPotentialScam
|
|
? 0.0
|
|
: fiatConversionStore.prices[key == CryptoCurrency.btcln ? CryptoCurrency.btc : key] ?? 0;
|
|
|
|
// if (price == null) {
|
|
// 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
|
|
? ''
|
|
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(available))}';
|
|
|
|
final additionalFiatBalance = isFiatDisabled
|
|
? ''
|
|
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(additional))}';
|
|
|
|
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 secondAdditionalFiatBalance = isFiatDisabled
|
|
? ''
|
|
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAdditional != null ? key.formatAmount(value.secondAdditional!) : null)}';
|
|
|
|
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,
|
|
asset: key,
|
|
secondAsset: secondAsset,
|
|
formattedAssetTitle: _formatterAsset(key),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
@observable
|
|
bool mwebEnabled = false;
|
|
|
|
bool hasAdditionalBalance(CryptoCurrency currency) {
|
|
final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type);
|
|
final isNotZeroAmount = additionalBalance(currency) != "0.0";
|
|
|
|
return isWalletTypeActivated && isNotZeroAmount;
|
|
}
|
|
|
|
@computed
|
|
bool get hasSecondAdditionalBalance {
|
|
if (wallet.type == WalletType.litecoin && mwebEnabled) {
|
|
return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0;
|
|
} else if (wallet.type == WalletType.bitcoin) {
|
|
return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@computed
|
|
bool get hasSecondAvailableBalance {
|
|
switch (wallet.type) {
|
|
case WalletType.bitcoin:
|
|
return true;
|
|
case WalletType.litecoin:
|
|
return mwebEnabled;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool _hasAdditionalBalanceForWalletType(WalletType type) => [
|
|
WalletType.monero,
|
|
WalletType.wownero,
|
|
].contains(type);
|
|
|
|
String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, BigInt? amount) {
|
|
if (amount == null) return "";
|
|
|
|
return appStore.amountParsingProxy
|
|
.getDisplayCryptoStringFromBigInt(amount, cryptoCurrency)
|
|
.withMaxDecimals(8).withLocalSeperator(settingsStore.languageCode);
|
|
}
|
|
|
|
@computed
|
|
List<BalanceRecord> get formattedBalances {
|
|
final balance = balances.values.toList();
|
|
|
|
balance.sort((BalanceRecord a, BalanceRecord b) {
|
|
if (wallet.currency == CryptoCurrency.xhv) {
|
|
if (b.asset == CryptoCurrency.xhv) return 1;
|
|
|
|
if (b.asset == CryptoCurrency.xusd) {
|
|
if (a.asset == CryptoCurrency.xhv) return -1;
|
|
return 1;
|
|
}
|
|
|
|
if (b.asset == CryptoCurrency.xbtc) return 1;
|
|
if (b.asset == CryptoCurrency.xeur) return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
if (pinNativeToken) {
|
|
if (b.asset == wallet.currency) return 1;
|
|
if (a.asset == wallet.currency) return -1;
|
|
}
|
|
|
|
final isTokenWallet = isEVMCompatibleChain(wallet.type);
|
|
|
|
if (isTokenWallet) {
|
|
final aIsToken = a.asset is Erc20Token || a.asset is SPLToken;
|
|
final bIsToken = b.asset is Erc20Token || b.asset is SPLToken;
|
|
|
|
final aHasBalance = (double.tryParse(a.availableBalance) ?? 0) > 0;
|
|
final bHasBalance = (double.tryParse(b.availableBalance) ?? 0) > 0;
|
|
|
|
// Adding this so tokens with balance come before tokens without balance
|
|
if (aIsToken && bIsToken) {
|
|
if (aHasBalance && !bHasBalance) return -1;
|
|
if (!aHasBalance && bHasBalance) return 1;
|
|
}
|
|
}
|
|
|
|
switch (sortBalanceBy) {
|
|
case SortBalanceBy.FiatBalance:
|
|
final aFiatBalance = _getFiatBalance(
|
|
price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance);
|
|
final bFiatBalance = _getFiatBalance(
|
|
price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance);
|
|
|
|
return (double.tryParse(bFiatBalance) ?? 0)
|
|
.compareTo((double.tryParse(aFiatBalance)) ?? 0);
|
|
case SortBalanceBy.GrossBalance:
|
|
return (double.tryParse(b.availableBalance) ?? 0)
|
|
.compareTo(double.tryParse(a.availableBalance) ?? 0);
|
|
case SortBalanceBy.Alphabetical:
|
|
return a.asset.title.compareTo(b.asset.title);
|
|
}
|
|
});
|
|
|
|
return balance;
|
|
}
|
|
|
|
BalanceRecord? getMainBalanceRecord(bool lightningMode) {
|
|
if (lightningMode) {
|
|
return formattedBalances.elementAtOrNull(1);
|
|
}
|
|
|
|
if (wallet.walletInfo.favoriteTokenAddress != null) {
|
|
return formattedBalances.firstWhereOrNull((item) => (getTokenAddressBasedOnWallet(item.asset) ==
|
|
wallet.walletInfo.favoriteTokenAddress)) ??
|
|
formattedBalances.elementAt(0);
|
|
}
|
|
|
|
return formattedBalances.elementAt(0);
|
|
}
|
|
|
|
String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
|
|
if (isEVMCompatibleChain(wallet.type) && asset is Erc20Token) {
|
|
return evm!.getTokenAddress(asset);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@computed
|
|
bool get showCombinedBalance {
|
|
if(wallet.type == WalletType.bitcoin) return false;
|
|
if(balances.values.length == 1) return false;
|
|
|
|
return wallet.walletInfo.showCombinedBalance;
|
|
}
|
|
|
|
@computed
|
|
String get combinedFiatBalance {
|
|
if (displayMode == BalanceDisplayMode.hiddenBalance) {
|
|
return "●●●●●";
|
|
}
|
|
|
|
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 price = fiatConversionStore.prices[curr] ?? 0;
|
|
ret += double.tryParse(calculateFiatAmount(
|
|
price: price, cryptoAmount: curr.formatAmount(available).replaceAll(",", ""))
|
|
.replaceAll(",", "")) ??
|
|
0;
|
|
}
|
|
return ret.toStringAsFixed(2);
|
|
}
|
|
|
|
Balance _currencyBalance(CryptoCurrency cryptoCurrency) {
|
|
final balance = wallet.balance[cryptoCurrency];
|
|
|
|
if (balance == null) throw Exception('No balance for ${wallet.currency}');
|
|
|
|
return balance;
|
|
}
|
|
|
|
@observable
|
|
bool isShowCard;
|
|
|
|
ReactionDisposer? _onCurrentWalletChangeReaction;
|
|
|
|
@action
|
|
void _onWalletChange(
|
|
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
|
|
if (wallet == null) return;
|
|
|
|
this.wallet = wallet;
|
|
_onCurrentWalletChangeReaction?.reaction.dispose();
|
|
isShowCard = wallet.walletInfo.isShowIntroCakePayCard;
|
|
}
|
|
|
|
@action
|
|
Future<void> disableIntroCakePayCard() async {
|
|
const cardDisplayStatus = false;
|
|
wallet.walletInfo.showIntroCakePayCard = cardDisplayStatus;
|
|
await wallet.walletInfo.save();
|
|
isShowCard = cardDisplayStatus;
|
|
}
|
|
|
|
@action
|
|
void switchBalanceValue() {
|
|
if (settingsStore.balanceDisplayMode == BalanceDisplayMode.displayableBalance) {
|
|
settingsStore.balanceDisplayMode = BalanceDisplayMode.hiddenBalance;
|
|
settingsStore.balanceHideCounter++;
|
|
} else {
|
|
settingsStore.balanceDisplayMode = BalanceDisplayMode.displayableBalance;
|
|
}
|
|
}
|
|
|
|
String _getFiatBalance({required double price, String? cryptoAmount}) {
|
|
if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) {
|
|
return '0.00'.withLocalSeperator(settingsStore.languageCode);
|
|
}
|
|
|
|
return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount)
|
|
.withLocalSeperator(settingsStore.languageCode);
|
|
}
|
|
|
|
String _formatterAsset(CryptoCurrency asset) {
|
|
return appStore.amountParsingProxy.getCryptoSymbol(asset);
|
|
}
|
|
}
|