dev
dart 566 lines 17.8 KB
Raw
1 import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 import 'package:cake_wallet/core/utilities.dart';
3 import 'package:cake_wallet/entities/balance_display_mode.dart';
4 import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
5 import 'package:cake_wallet/entities/fiat_api_mode.dart';
6 import 'package:cake_wallet/entities/fiat_currency.dart';
7 import 'package:cake_wallet/entities/sort_balance_types.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/reactions/wallet_connect.dart';
10 import 'package:cake_wallet/evm/evm.dart';
11 import 'package:cake_wallet/solana/solana.dart';
12 import 'package:cake_wallet/tron/tron.dart';
13 import 'package:cake_wallet/zano/zano.dart';
14 import 'package:cw_core/amount/money.dart';
15 import 'package:cw_core/crypto_amount_format.dart';
16 import 'package:cw_core/transaction_history.dart';
17 import 'package:cw_core/wallet_base.dart';
18 import 'package:cake_wallet/store/app_store.dart';
19 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
20 import 'package:cake_wallet/store/settings_store.dart';
21 import 'package:cw_core/balance.dart';
22 import 'package:cw_core/crypto_currency.dart';
23 import 'package:cw_core/erc20_token.dart';
24 import 'package:cw_core/spl_token.dart';
25 import 'package:cw_core/transaction_info.dart';
26 import 'package:cw_core/wallet_type.dart';
27 import 'package:mobx/mobx.dart';
28
29 part 'balance_view_model.g.dart';
30
31 class BalanceRecord {
32 const BalanceRecord({
33 required this.raw,
34 required this.availableBalance,
35 required this.additionalBalance,
36 required this.secondAvailableBalance,
37 required this.secondAdditionalBalance,
38 required this.frozenBalance,
39 required this.fiatAvailableBalanceRaw,
40 required this.fiatAdditionalBalanceRaw,
41 required this.fiatFrozenBalanceRaw,
42 required this.fiatSecondAvailableBalanceRaw,
43 required this.fiatSecondAdditionalBalanceRaw,
44 required this.asset,
45 required this.secondAsset,
46 required this.fiatCurrency,
47 required this.formattedAssetTitle,
48 this.languageCode,
49 });
50
51 final Balance raw;
52
53 final String fiatAvailableBalanceRaw;
54 final String fiatAdditionalBalanceRaw;
55 final String fiatFrozenBalanceRaw;
56 final String fiatSecondAvailableBalanceRaw;
57 final String fiatSecondAdditionalBalanceRaw;
58
59 final String additionalBalance;
60 final String availableBalance;
61 final String frozenBalance;
62 final String secondAvailableBalance;
63 final String secondAdditionalBalance;
64 final CryptoCurrency asset;
65 final CryptoCurrency secondAsset;
66 final FiatCurrency? fiatCurrency;
67 final String formattedAssetTitle;
68 final String? languageCode;
69
70 String get combinedAvailableBalance =>
71 (raw.available + (raw.secondAvailable ?? Money.zero(raw.available.currency)))
72 .toString()
73 .withMaxDecimals(8);
74
75 String get combinedFiatAvailableBalance => fiatCurrency != null
76 ? "$fiatCurrency " +
77 _withLocalSeperator(((double.tryParse(fiatAvailableBalanceRaw) ?? 0) +
78 (double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0))
79 .toStringAsFixed(2))
80 : "";
81
82 String get fiatAvailableBalance =>
83 fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAvailableBalanceRaw)}" : "";
84
85 String get fiatAdditionalBalance =>
86 fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAdditionalBalanceRaw)}" : "";
87
88 String get fiatFrozenBalance =>
89 fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatFrozenBalanceRaw)}" : "";
90
91 String get fiatSecondAvailableBalance => fiatCurrency != null
92 ? "$fiatCurrency ${_withLocalSeperator(fiatSecondAvailableBalanceRaw)}"
93 : "";
94
95 String get fiatSecondAdditionalBalance => fiatCurrency != null
96 ? "$fiatCurrency ${_withLocalSeperator(fiatSecondAdditionalBalanceRaw)}"
97 : "";
98
99 String _withLocalSeperator(String rawAmount) =>
100 languageCode == null ? rawAmount : rawAmount.withLocalSeperator(languageCode);
101 }
102
103 class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel;
104
105 abstract class BalanceViewModelBase with Store {
106 BalanceViewModelBase(
107 {required this.appStore, required this.settingsStore, required this.fiatConversionStore})
108 : isReversing = false,
109 isShowCard = appStore.wallet?.walletInfo.isShowIntroCakePayCard ?? false,
110 wallet = appStore.wallet! {
111 reaction((_) => appStore.wallet, (wallet) {
112 _onWalletChange(wallet);
113 _checkMweb();
114 });
115
116 _checkMweb();
117
118 reaction((_) => settingsStore.mwebAlwaysScan, (_) => _checkMweb());
119 }
120
121 void _checkMweb() {
122 if (wallet.type == WalletType.litecoin) {
123 mwebEnabled = bitcoin!.getMwebEnabled(wallet);
124 }
125 }
126
127 final AppStore appStore;
128 final SettingsStore settingsStore;
129 final FiatConversionStore fiatConversionStore;
130
131 @observable
132 bool isReversing;
133
134 @observable
135 WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
136
137 @computed
138 double get price {
139 final price = fiatConversionStore.prices[appStore.wallet!.currency];
140
141 // price should update on next fetch:
142 if (price == null) return 0;
143
144 return price;
145 }
146
147 @computed
148 BalanceDisplayMode get savedDisplayMode => settingsStore.balanceDisplayMode;
149
150 @computed
151 bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled;
152
153 @computed
154 bool get isHomeScreenSettingsEnabled =>
155 isEVMCompatibleChain(wallet.type) ||
156 [WalletType.solana, WalletType.tron, WalletType.zano].contains(wallet.type);
157
158 @computed
159 bool get isEVMCompatible => isEVMCompatibleChain(wallet.type);
160
161 @computed
162 bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type);
163
164 @computed
165 SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy;
166
167 @computed
168 bool get pinNativeToken => settingsStore.pinNativeTokenAtTop;
169
170 @computed
171 String get asset {
172 if (isEVMCompatibleChain(wallet.type)) {
173 final currentChain = evm!.getCurrentChain(wallet);
174 if (currentChain != null) {
175 return currentChain.name;
176 }
177
178 return walletTypeToString(wallet.type);
179 }
180
181 final typeFormatted = walletTypeToString(wallet.type);
182
183 switch (wallet.type) {
184 case WalletType.haven:
185 return '$typeFormatted Assets';
186 default:
187 return typeFormatted;
188 }
189 }
190
191 @computed
192 BalanceDisplayMode get displayMode {
193 if (isReversing) {
194 if (savedDisplayMode == BalanceDisplayMode.hiddenBalance) {
195 return BalanceDisplayMode.displayableBalance;
196 } else {
197 return BalanceDisplayMode.hiddenBalance;
198 }
199 }
200
201 return savedDisplayMode;
202 }
203
204 @computed
205 String get availableBalanceLabel {
206 if (displayMode == BalanceDisplayMode.hiddenBalance) {
207 return S.current.show_balance;
208 } else {
209 return S.current.xmr_available_balance;
210 }
211 }
212
213 @computed
214 String get additionalBalanceLabel {
215 switch (wallet.type) {
216 case WalletType.haven:
217 case WalletType.ethereum:
218 case WalletType.polygon:
219 case WalletType.base:
220 case WalletType.arbitrum:
221 case WalletType.bsc:
222 case WalletType.solana:
223 case WalletType.tron:
224 return S.current.xmr_full_balance;
225 case WalletType.nano:
226 case WalletType.banano:
227 return S.current.receivable_balance;
228 default:
229 return S.current.unconfirmed;
230 }
231 }
232
233 @computed
234 String get secondAvailableBalanceLabel {
235 switch (wallet.type) {
236 case WalletType.litecoin:
237 return S.current.mweb_confirmed;
238 default:
239 return S.current.confirmed;
240 }
241 }
242
243 @computed
244 String get secondAdditionalBalanceLabel {
245 switch (wallet.type) {
246 case WalletType.litecoin:
247 return S.current.mweb_unconfirmed;
248 default:
249 return S.current.unconfirmed;
250 }
251 }
252
253 Money additionalBalance(CryptoCurrency cryptoCurrency) {
254 final balance = _currencyBalance(cryptoCurrency);
255
256 if (displayMode == BalanceDisplayMode.hiddenBalance || balance.unavailable.isZero)
257 return Money.zero(cryptoCurrency);
258
259 return balance.unavailable;
260 }
261
262 @computed
263 Map<CryptoCurrency, BalanceRecord> get balances {
264 return wallet.balance.map((key, value) {
265 var secondAsset = key == CryptoCurrency.ltc ? CryptoCurrency.ltcmweb : key;
266
267 if (displayMode == BalanceDisplayMode.hiddenBalance) {
268 final fiatCurrency = settingsStore.fiatCurrency;
269 return MapEntry(
270 key,
271 BalanceRecord(
272 raw: value,
273 availableBalance: '●●●●●●',
274 additionalBalance: '',
275 frozenBalance: '',
276 secondAvailableBalance: '●●●●●●',
277 secondAdditionalBalance: '●●●●●●',
278 fiatAdditionalBalanceRaw: '●●●●●',
279 fiatAvailableBalanceRaw: '●●●●●',
280 fiatFrozenBalanceRaw: '',
281 fiatSecondAvailableBalanceRaw: '●●●●●',
282 fiatSecondAdditionalBalanceRaw: '●●●●●',
283 asset: key,
284 secondAsset: secondAsset,
285 fiatCurrency: isFiatDisabled ? null : fiatCurrency,
286 formattedAssetTitle: _formatterAsset(key),
287 ),
288 );
289 }
290 final price = key.isPotentialScam
291 ? 0.0
292 : fiatConversionStore.prices[key == CryptoCurrency.btcln ? CryptoCurrency.btc : key] ?? 0;
293
294 // if (price == null) {
295 // throw Exception('Price is null for: $key');
296 // }
297
298 final availableFiatBalance =
299 isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.available);
300
301 final additionalFiatBalance =
302 isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.unavailable);
303
304 final frozenFiatBalance =
305 isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.frozen);
306
307 final secondAvailableFiatBalance =
308 isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.secondAvailable);
309
310 final secondAdditionalFiatBalance = isFiatDisabled
311 ? ''
312 : _getFiatBalance(price: price, cryptoAmount: value.secondUnavailable);
313
314 return MapEntry(
315 key,
316 BalanceRecord(
317 raw: value,
318 availableBalance: _getFormattedCryptoAmount(value.available),
319 fiatAvailableBalanceRaw: availableFiatBalance,
320 additionalBalance: _getFormattedCryptoAmount(value.unavailable),
321 fiatAdditionalBalanceRaw: additionalFiatBalance,
322 frozenBalance: _getFormattedCryptoAmount(value.frozen),
323 fiatFrozenBalanceRaw: frozenFiatBalance,
324 secondAvailableBalance: _getFormattedCryptoAmount(value.secondAvailable),
325 fiatSecondAvailableBalanceRaw: secondAvailableFiatBalance,
326 secondAdditionalBalance: _getFormattedCryptoAmount(value.secondUnavailable),
327 fiatSecondAdditionalBalanceRaw: secondAdditionalFiatBalance,
328 asset: key,
329 secondAsset: secondAsset,
330 fiatCurrency: isFiatDisabled ? null : settingsStore.fiatCurrency,
331 formattedAssetTitle: _formatterAsset(key),
332 languageCode: settingsStore.languageCode,
333 ),
334 );
335 });
336 }
337
338 @observable
339 bool mwebEnabled = false;
340
341 bool hasAdditionalBalance(CryptoCurrency currency) {
342 final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type);
343 final isNotZeroAmount = !additionalBalance(currency).isZero;
344
345 return isWalletTypeActivated && isNotZeroAmount;
346 }
347
348 @computed
349 bool get hasSecondAdditionalBalance {
350 if (wallet.type == WalletType.litecoin && mwebEnabled) {
351 return (wallet.balance[CryptoCurrency.ltc]?.secondUnavailable ?? 0) != 0;
352 } else if (wallet.type == WalletType.bitcoin) {
353 return (wallet.balance[CryptoCurrency.btc]?.secondUnavailable ?? 0) != 0;
354 }
355 return false;
356 }
357
358 @computed
359 bool get hasSecondAvailableBalance {
360 switch (wallet.type) {
361 case WalletType.bitcoin:
362 return true;
363 case WalletType.litecoin:
364 return mwebEnabled;
365 default:
366 return false;
367 }
368 }
369
370 bool _hasAdditionalBalanceForWalletType(WalletType type) => [
371 WalletType.monero,
372 WalletType.wownero,
373 WalletType.zano,
374 WalletType.decred,
375 WalletType.zcash
376 ].contains(type);
377
378 String _getFormattedCryptoAmount(Money? amount) {
379 if (amount == null) return "";
380
381 return appStore.amountParsingProxy
382 .asDisplayString(amount)
383 .withLocalSeperator(settingsStore.languageCode);
384 }
385
386 @computed
387 List<BalanceRecord> get formattedBalances {
388 final balance = balances.values.toList();
389
390 balance.sort((BalanceRecord a, BalanceRecord b) {
391 if (wallet.currency == CryptoCurrency.xhv) {
392 if (b.asset == CryptoCurrency.xhv) return 1;
393
394 if (b.asset == CryptoCurrency.xusd) {
395 if (a.asset == CryptoCurrency.xhv) return -1;
396 return 1;
397 }
398
399 if (b.asset == CryptoCurrency.xbtc) return 1;
400 if (b.asset == CryptoCurrency.xeur) return 1;
401
402 return 0;
403 }
404
405 if (pinNativeToken) {
406 if (b.asset == wallet.currency) return 1;
407 if (a.asset == wallet.currency) return -1;
408 }
409
410 final isTokenWallet = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana;
411
412 if (isTokenWallet) {
413 final aIsToken = a.asset is Erc20Token || a.asset is SPLToken;
414 final bIsToken = b.asset is Erc20Token || b.asset is SPLToken;
415
416 final aHasBalance = (double.tryParse(a.availableBalance) ?? 0) > 0;
417 final bHasBalance = (double.tryParse(b.availableBalance) ?? 0) > 0;
418
419 // Adding this so tokens with balance come before tokens without balance
420 if (aIsToken && bIsToken) {
421 if (aHasBalance && !bHasBalance) return -1;
422 if (!aHasBalance && bHasBalance) return 1;
423 }
424 }
425
426 switch (sortBalanceBy) {
427 case SortBalanceBy.FiatBalance:
428 final aFiatBalance = _getFiatBalance(
429 price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.raw.available);
430 final bFiatBalance = _getFiatBalance(
431 price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.raw.available);
432
433 return (double.tryParse(bFiatBalance) ?? 0)
434 .compareTo((double.tryParse(aFiatBalance)) ?? 0);
435 case SortBalanceBy.GrossBalance:
436 return (double.tryParse(b.availableBalance) ?? 0)
437 .compareTo(double.tryParse(a.availableBalance) ?? 0);
438 case SortBalanceBy.Alphabetical:
439 return a.asset.title.compareTo(b.asset.title);
440 }
441 });
442
443 return balance;
444 }
445
446 BalanceRecord? getMainBalanceRecord(bool lightningMode) {
447 if (lightningMode) {
448 return formattedBalances.elementAtOrNull(1);
449 }
450
451 if (wallet.walletInfo.favoriteTokenAddress != null) {
452 return formattedBalances.firstWhereOrNull((item) =>
453 (getTokenAddressBasedOnWallet(item.asset) ==
454 wallet.walletInfo.favoriteTokenAddress)) ??
455 formattedBalances.elementAtOrNull(0);
456 }
457
458 return formattedBalances.elementAtOrNull(0);
459 }
460
461 String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
462 if (wallet.type == WalletType.tron) {
463 return tron!.getTokenAddress(asset);
464 }
465
466 if (wallet.type == WalletType.solana) {
467 return solana!.getTokenAddress(asset);
468 }
469
470 if (isEVMCompatibleChain(wallet.type) && asset is Erc20Token) {
471 return evm!.getTokenAddress(asset);
472 }
473
474 if (wallet.type == WalletType.zano) {
475 return zano!.getZanoAssetAddress(asset);
476 }
477
478 return null;
479 }
480
481 @computed
482 bool get showCombinedBalance {
483 if (wallet.type == WalletType.bitcoin) return false;
484 if (balances.values.length == 1) return false;
485
486 return wallet.walletInfo.showCombinedBalance;
487 }
488
489 @computed
490 String get combinedFiatBalance {
491 if (displayMode == BalanceDisplayMode.hiddenBalance) {
492 return "●●●●●";
493 }
494
495 double ret = 0.0;
496 for (final curr in wallet.balance.keys) {
497 final record = wallet.balance[curr]!;
498 final available = record.available - (record.secondAvailable ?? Money.zero(curr));
499 final price = fiatConversionStore.prices[curr] ?? 0;
500 ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString())
501 .replaceAll(",", "")) ??
502 0;
503 }
504 return ret.toStringAsFixed(2).withLocalSeperator(settingsStore.languageCode);
505 }
506
507 Balance _currencyBalance(CryptoCurrency cryptoCurrency) {
508 final balance = wallet.balance[cryptoCurrency];
509
510 if (balance == null) throw Exception('No balance for ${wallet.currency}');
511
512 return balance;
513 }
514
515 @observable
516 bool isShowCard;
517
518 ReactionDisposer? _onCurrentWalletChangeReaction;
519
520 @action
521 void _onWalletChange(
522 WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
523 if (wallet == null) return;
524
525 this.wallet = wallet;
526 _onCurrentWalletChangeReaction?.reaction.dispose();
527 isShowCard = wallet.walletInfo.isShowIntroCakePayCard;
528 }
529
530 @action
531 Future<void> disableIntroCakePayCard() async {
532 const cardDisplayStatus = false;
533 wallet.walletInfo.showIntroCakePayCard = cardDisplayStatus;
534 await wallet.walletInfo.save();
535 isShowCard = cardDisplayStatus;
536 }
537
538 @action
539 void switchBalanceValue() {
540 if (settingsStore.balanceDisplayMode == BalanceDisplayMode.displayableBalance) {
541 settingsStore.balanceDisplayMode = BalanceDisplayMode.hiddenBalance;
542 settingsStore.balanceHideCounter++;
543 } else {
544 settingsStore.balanceDisplayMode = BalanceDisplayMode.displayableBalance;
545 }
546 }
547
548 String _getFiatBalance({required double price, Money? cryptoAmount}) {
549 if (cryptoAmount == null) {
550 return '0.00';
551 }
552
553 return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount.toString(), raw: true);
554 }
555
556 String _formatterAsset(CryptoCurrency asset) {
557 final assetString = asset.toString();
558 if (wallet.type == WalletType.haven &&
559 asset != CryptoCurrency.xhv &&
560 assetString[0].toUpperCase() == 'X') {
561 return assetString.replaceFirst('X', 'x');
562 }
563
564 return appStore.amountParsingProxy.getCryptoSymbol(asset);
565 }
566 }