Merge upstream Cake Wallet v6.5.0

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

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

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

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
@@ -1,208 +0,0 @@
import 'package:hash_wallet/anonpay/anonpay_api.dart';
import 'package:hash_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:hash_wallet/anonpay/anonpay_request.dart';
import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/utils/qr_util.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'anon_invoice_page_view_model.g.dart';
class AnonInvoicePageViewModel = AnonInvoicePageViewModelBase with _$AnonInvoicePageViewModel;
abstract class AnonInvoicePageViewModelBase with Store {
AnonInvoicePageViewModelBase(
this.anonPayApi,
this.address,
this.settingsStore,
this._wallet,
this._anonpayInvoiceInfoSource,
this.sharedPreferences,
this.pageOption,
) : receipientEmail = '',
receipientName = '',
description = '',
amount = '',
state = InitialExecutionState(),
selectedCurrency = _wallet.currency,
cryptoCurrency = _wallet.currency {
_getPreviousDonationLink();
_fetchLimits();
}
List<Currency> get currencies => [_wallet.currency, ...FiatCurrency.all];
final AnonPayApi anonPayApi;
final String address;
final SettingsStore settingsStore;
final WalletBase _wallet;
final Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
final SharedPreferences sharedPreferences;
final ReceivePageOption pageOption;
@observable
Currency selectedCurrency;
CryptoCurrency cryptoCurrency;
@observable
String receipientEmail;
@observable
String receipientName;
@observable
String description;
@observable
String amount;
@observable
ExecutionState state;
@computed
int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency);
@observable
double? minimum;
@observable
double? maximum;
@action
void selectCurrency(Currency currency) {
selectedCurrency = currency;
maximum = minimum = null;
if (currency is CryptoCurrency) {
cryptoCurrency = currency;
} else {
cryptoCurrency = _wallet.currency;
}
_fetchLimits();
}
@action
Future<void> createInvoice() async {
state = IsExecutingState();
if (amount.isNotEmpty) {
final amountInCrypto = double.tryParse(amount);
if (amountInCrypto == null) {
state = FailureState('Amount is invalid');
return;
}
if (minimum != null && amountInCrypto < minimum!) {
state = FailureState('Amount is too small');
return;
}
if (maximum != null && amountInCrypto > maximum!) {
state = FailureState('Amount is too big');
return;
}
}
try {
final result = await anonPayApi.createInvoice(AnonPayRequest(
cryptoCurrency: cryptoCurrency,
address: address,
amount: amount.isEmpty ? null : amount,
description: description,
email: receipientEmail,
name: receipientName,
fiatEquivalent:
selectedCurrency is FiatCurrency ? (selectedCurrency as FiatCurrency).raw : null,
));
_anonpayInvoiceInfoSource.add(result);
state = ExecutedSuccessfullyState(payload: result);
} catch (e) {
state = FailureState(e.toString());
}
}
@action
void setRequestParams({
required String inputAmount,
required String inputName,
required String inputEmail,
required String inputDescription,
}) {
receipientName = inputName;
receipientEmail = inputEmail;
description = inputDescription;
amount = inputAmount;
}
@action
Future<void> generateDonationLink() async {
state = IsExecutingState();
final result = await anonPayApi.generateDonationLink(AnonPayRequest(
cryptoCurrency: cryptoCurrency,
address: address,
description: description,
email: receipientEmail,
name: receipientName,
));
await sharedPreferences.setString(PreferencesKey.clearnetDonationLink, result.clearnetUrl);
await sharedPreferences.setString(PreferencesKey.onionDonationLink, result.onionUrl);
await sharedPreferences.setString(PreferencesKey.donationLinkWalletName, _wallet.name);
state = ExecutedSuccessfullyState(payload: result);
}
Future<void> _fetchLimits() async {
try {
final limit = await anonPayApi.fetchLimits(
cryptoCurrency: cryptoCurrency,
fiatCurrency: selectedCurrency is FiatCurrency ? selectedCurrency as FiatCurrency : null,
);
minimum = limit.min;
maximum = limit.max != null ? limit.max! / 4 : null;
} catch (e) {
state = FailureState(e.toString());
}
}
@computed
String get currentWalletName => _wallet.name;
@computed
String get qrImage => getQrImage(_wallet.type);
@action
void reset() {
selectedCurrency = _wallet.currency;
cryptoCurrency = _wallet.currency;
receipientEmail = '';
receipientName = '';
description = '';
amount = '';
_fetchLimits();
}
Future<void> _getPreviousDonationLink() async {
if (pageOption == ReceivePageOption.anonPayDonationLink) {
final donationLink = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
final donationLinkWalletName =
sharedPreferences.getString(PreferencesKey.donationLinkWalletName);
if (donationLink != null && currentWalletName == donationLinkWalletName) {
final url = Uri.parse(donationLink);
url.queryParameters.forEach((key, value) {
if (key == 'name') receipientName = value;
if (key == 'email') receipientEmail = value;
if (key == 'description') description = Uri.decodeComponent(value);
});
}
}
}
}
+14 -19
View File
@@ -17,6 +17,8 @@ import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/bridge_transfers_store.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
@@ -133,9 +135,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
String get tokenBalanceFormatted {
if (selectedToken == null) return "0.00";
return amountParsingProxy.getDisplayCryptoStringFromBigInt(
selectedTokenBalance,
selectedToken!,
return amountParsingProxy.asDisplayString(
Money(selectedTokenBalance, selectedToken!),
);
}
@@ -175,10 +176,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
String get quoteNativeFee {
if (quote == null) return '—';
return amountParsingProxy.getDisplayCryptoStringFromBigInt(
quote!.nativeFee,
wallet.currency,
);
return amountParsingProxy.asDisplayString(Money(quote!.nativeFee, wallet.currency));
}
@computed
@@ -219,10 +217,10 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
if (amountError != null) return false;
final validAmount = amountParsingProxy.tryParseCryptoString(
amount.replaceAll(',', '.'),
amount.sanitized(),
selectedToken!,
);
return validAmount != null && validAmount > BigInt.zero;
return validAmount != null && validAmount > Money.zero(selectedToken!);
}
@computed
@@ -232,7 +230,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
try {
final bal = wallet.balance[selectedToken!];
return bal?.fullAvailableBalance ?? BigInt.zero;
return bal?.available.amount ?? BigInt.zero;
} catch (e) {
return BigInt.zero;
}
@@ -248,7 +246,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
);
if (amountBigInt == null || amountBigInt == BigInt.zero) return null;
if (amountBigInt > selectedTokenBalance) {
if (amountBigInt.amount > selectedTokenBalance) {
return 'Insufficient balance for ${selectedToken!.title} token.';
}
@@ -282,10 +280,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
return;
}
setAmount(
amountParsingProxy.getDisplayCryptoStringFromBigInt(
selectedTokenBalance,
token,
),
amountParsingProxy.asDisplayString(Money(selectedTokenBalance, token)),
);
}
@@ -383,23 +378,23 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
}
({String? error, BigInt? parsedAmount}) _parseAndValidateAmount(Erc20Token token) {
final amountBigInt = amountParsingProxy.tryParseCryptoString(
final parsedAmount = amountParsingProxy.tryParseCryptoString(
amount.replaceAll(',', '.'),
token,
);
if (amountBigInt == null || amountBigInt == BigInt.zero) {
if (parsedAmount == null || parsedAmount == Money(BigInt.zero, token)) {
return (error: 'Invalid amount', parsedAmount: null);
}
if (amountBigInt > selectedTokenBalance) {
if (parsedAmount.amount > selectedTokenBalance) {
return (
error: 'Insufficient balance for ${token.title} token.',
parsedAmount: null,
);
}
return (error: null, parsedAmount: amountBigInt);
return (error: null, parsedAmount: parsedAmount.amount);
}
@action
-29
View File
@@ -1,29 +0,0 @@
import 'package:hash_wallet/buy/buy_amount.dart';
import 'package:hash_wallet/buy/buy_provider.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/view_model/buy/buy_amount_view_model.dart';
import 'package:cw_core/utils/print_verbose.dart';
class BuyItem {
BuyItem({required this.provider, required this.buyAmountViewModel});
final BuyProvider provider;
final BuyAmountViewModel buyAmountViewModel;
double get amount => buyAmountViewModel.doubleAmount;
FiatCurrency get fiatCurrency => buyAmountViewModel.fiatCurrency;
Future<BuyAmount> get buyAmount async {
BuyAmount _buyAmount;
try {
_buyAmount = await provider.calculateAmount(amount?.toString() ?? '', fiatCurrency.title);
} catch (e) {
_buyAmount = BuyAmount(sourceAmount: 0.0, destAmount: 0.0);
printV(e.toString());
}
return _buyAmount;
}
}
+197 -117
View File
@@ -1,30 +1,39 @@
import 'dart:async';
import 'dart:math';
import 'package:hash_wallet/buy/buy_provider.dart';
import 'package:hash_wallet/buy/buy_quote.dart';
import 'package:hash_wallet/buy/payment_method.dart';
import 'package:hash_wallet/buy/sell_buy_states.dart';
import 'package:hash_wallet/core/amount_parsing_proxy.dart';
import 'package:hash_wallet/core/selectable_option.dart';
import 'package:hash_wallet/core/wallet_change_listener_view_model.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/entities/provider_types.dart';
import 'package:hash_wallet/generated/i18n.dart';
import "package:hash_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
import 'package:hash_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:hash_wallet/routes.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:mobx/mobx.dart';
part 'buy_sell_view_model.g.dart';
enum BuySellPageMode { buy, sell }
class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
BuySellViewModelBase(
AppStore appStore,
) : _cryptoAmount = '',
BuySellViewModelBase(AppStore appStore, {required NewBuySellParams params, required this.fiatConversionStore})
: _cryptoAmount = '',
fiatAmount = '',
mode = params.mode,
cryptoCurrencyAddress = '',
isCryptoCurrencyAddressEnabled = false,
cryptoCurrencies = <CryptoCurrency>[],
@@ -50,10 +59,16 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
_initialize();
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
if(params.initialCurrency != null) {
changeCryptoCurrency(currency: params.initialCurrency!);
}
}
late Timer bestRateSync;
final FiatConversionStore fiatConversionStore;
List<BuyProvider> get availableBuyProviders {
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes();
return providerTypes
@@ -79,13 +94,33 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
final formattedFiatAmount = double.tryParse(fiatAmount);
final formattedCryptoAmount = double.tryParse(_cryptoAmount);
return isBuyAction
return mode == BuySellPageMode.buy
? formattedFiatAmount ?? 200.0
: formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1);
: formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.01 : 1);
}
// sets based on the absolute amout (from the fiat/charts api)
// works even if you have no rates
Future<void> setCryptoAmountFromFiat(String fiatAmount) async {
if(fiatAmount.isEmpty) {
await changeCryptoAmount(amount: "");
return;
}
if(fiatConversionStore.prices[cryptoCurrency] == null) {
return;
}
await changeCryptoAmount(
amount: (double.parse(fiatAmount) / (fiatConversionStore.prices[cryptoCurrency]!))
.toString(),
);
}
final AppStore _appStore;
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
Quote? bestRateQuote;
Quote? selectedQuote;
@@ -96,8 +131,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
@observable
List<FiatCurrency> fiatCurrencies;
@observable
bool isBuyAction = true;
final BuySellPageMode mode;
@observable
List<BuyProvider> providerList;
@@ -145,6 +179,100 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
@observable
bool skipIsReadyToTradeReaction = false;
@computed
String? get maxFiatAmount {
if ((sortedQuotes.isEmpty && sortedRecommendedQuotes.isEmpty) ||
buySellQuotState is! BuySellQuotLoaded) {
return null;
}
final allQuotes = sortedRecommendedQuotes.followedBy(sortedQuotes);
final maxAmount = allQuotes.fold<double>(0.0, (current, item) {
final limitMax = item.limits?.max?.toDouble() ?? 0.0;
return max(current, limitMax);
});
return maxAmount.toStringAsFixed(2);
}
Money? amountForQuote(Quote quote) => Money.trySafeParse(
((double.tryParse(fiatAmount) ?? 0) / quote.rate).toStringAsFixed(min(20, cryptoCurrency.decimals)),
cryptoCurrency);
Money? fiatAmountForQuote(Quote quote) {
if(fiatConversionStore.prices[cryptoCurrency] == null) {
return null;
}
return Money.trySafeParse(
(fiatConversionStore.prices[cryptoCurrency]! *
(double.tryParse(amountForQuote(quote).toString())??0))
.toStringAsFixed(2),
fiatCurrency);
}
// based on usd values, should have roughly equal worth (was done with ai though so it's subject to correction)
static final Map<FiatCurrency, List<String>> _defaultAmountsMap = {
FiatCurrency.amd: ["20000", "40000", "200000", "400000", "1000000"],
FiatCurrency.ars: ["50000", "100000", "500000", "1000000", "2500000"],
FiatCurrency.aud: ["100", "200", "1000", "2000", "5000"],
FiatCurrency.bdt: ["5000", "10000", "50000", "100000", "250000"],
FiatCurrency.bgn: ["100", "200", "1000", "2000", "5000"],
FiatCurrency.brl: ["250", "500", "2500", "5000", "12500"],
FiatCurrency.cad: ["50", "100", "500", "1000", "2500"],
FiatCurrency.chf: ["50", "100", "500", "1000", "2500"],
FiatCurrency.clp: ["50000", "100000", "500000", "1000000", "2500000"],
FiatCurrency.cny: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.cop: ["200000", "400000", "2000000", "4000000", "10000000"],
FiatCurrency.czk: ["1000", "2000", "10000", "20000", "50000"],
FiatCurrency.dkk: ["400", "800", "4000", "8000", "20000"],
FiatCurrency.egp: ["2500", "5000", "25000", "50000", "125000"],
FiatCurrency.eur: ["50", "100", "500", "1000", "2500"],
FiatCurrency.gbp: ["50", "100", "500", "1000", "2500"],
FiatCurrency.ghs: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.gtq: ["400", "800", "4000", "8000", "20000"],
FiatCurrency.hkd: ["400", "800", "4000", "8000", "20000"],
FiatCurrency.hrk: ["400", "800", "4000", "8000", "20000"],
FiatCurrency.huf: ["20000", "40000", "200000", "400000", "1000000"],
FiatCurrency.idr: ["800000", "1600000", "8000000", "16000000", "40000000"],
FiatCurrency.ils: ["200", "400", "2000", "4000", "10000"],
FiatCurrency.inr: ["5000", "10000", "50000", "100000", "250000"],
FiatCurrency.irr: ["2000000", "4000000", "20000000", "40000000", "100000000"],
FiatCurrency.isk: ["7000", "14000", "70000", "140000", "350000"],
FiatCurrency.jpy: ["10000", "20000", "100000", "200000", "500000"],
FiatCurrency.kes: ["5000", "10000", "50000", "100000", "250000"],
FiatCurrency.krw: ["50000", "100000", "500000", "1000000", "2500000"],
FiatCurrency.mad: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.mxn: ["1000", "2000", "10000", "20000", "50000"],
FiatCurrency.myr: ["250", "500", "2500", "5000", "12500"],
FiatCurrency.ngn: ["50000", "100000", "500000", "1000000", "2500000"],
FiatCurrency.nok: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.nzd: ["100", "200", "1000", "2000", "5000"],
FiatCurrency.php: ["3000", "6000", "30000", "60000", "150000"],
FiatCurrency.pkr: ["15000", "30000", "150000", "300000", "750000"],
FiatCurrency.pln: ["200", "400", "2000", "4000", "10000"],
FiatCurrency.ron: ["250", "500", "2500", "5000", "12500"],
FiatCurrency.rub: ["5000", "10000", "50000", "100000", "250000"],
FiatCurrency.sar: ["200", "400", "2000", "4000", "10000"],
FiatCurrency.sek: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.sgd: ["50", "100", "500", "1000", "2500"],
FiatCurrency.thb: ["2000", "4000", "20000", "40000", "100000"],
FiatCurrency.tur: ["1500", "3000", "15000", "30000", "75000"],
FiatCurrency.twd: ["1500", "3000", "15000", "30000", "75000"],
FiatCurrency.uah: ["2000", "4000", "20000", "40000", "100000"],
FiatCurrency.usd: ["50", "100", "500", "1000", "2500"],
FiatCurrency.vef: ["2000", "4000", "20000", "40000", "100000"],
FiatCurrency.vnd: ["1000000", "2000000", "10000000", "20000000", "50000000"],
FiatCurrency.zar: ["1000", "2000", "10000", "20000", "50000"],
};
// the fallback is just the usd values.
// not great but this fallback shouldn't be triggered anyway
List<String> get defaultAmounts =>
_defaultAmountsMap[fiatCurrency] ?? ["50", "100", "500", "1000", "2500"];
@computed
bool get isReadyToTrade {
final hasSelectedQuote = selectedQuote != null;
@@ -177,12 +305,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
_initialize();
}
@action
void changeBuySellAction() {
isBuyAction = !isBuyAction;
_initialize();
}
@action
void changeFiatCurrency({required FiatCurrency currency}) {
fiatCurrency = currency;
@@ -196,12 +318,18 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
@computed
Iterable<CryptoCurrency> get activeWalletCurrencies => wallet.balance.keys;
@computed
bool get hasMultipleCurrencies => activeWalletCurrencies.length > 1;
@action
void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address;
@action
Future<void> changeFiatAmount({required String amount}) async {
fiatAmount = amount;
fiatAmount = amount.replaceAll(",", ".");
if (amount.isEmpty) {
fiatAmount = '';
@@ -210,16 +338,18 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
}
if (!isReadyToTrade && !isBuySellQuoteFailed) {
_cryptoAmount = S.current.fetching;
_cryptoAmount = "...";
return;
} else if (isBuySellQuoteFailed) {
_cryptoAmount = '';
return;
}
printV(bestRateQuote);
if (bestRateQuote != null) {
final enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0;
final amount = enteredAmount / bestRateQuote!.rate;
printV(amount);
_cryptoAmount = amount.toString().withMaxDecimals(cryptoCurrency.decimals);
} else {
@@ -271,70 +401,13 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
}
}
void onTapChoseProvider(BuildContext context) async {
skipIsReadyToTradeReaction = true;
final initialQuotes = List<Quote>.from(sortedRecommendedQuotes + sortedQuotes);
await calculateBestRate();
final newQuotes = (sortedRecommendedQuotes + sortedQuotes);
for (var quote in newQuotes) quote.limits = null;
final newQuoteProviders = newQuotes
.map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title)
.toSet();
final outOfLimitQuotes = initialQuotes.where((initialQuote) {
return !newQuoteProviders.contains(
initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title);
}).map((missingQuote) {
final quote = Quote(
rate: missingQuote.rate,
feeAmount: missingQuote.feeAmount,
networkFee: missingQuote.networkFee,
transactionFee: missingQuote.transactionFee,
payout: missingQuote.payout,
rampId: missingQuote.rampId,
rampName: missingQuote.rampName,
rampIconPath: missingQuote.rampIconPath,
paymentType: missingQuote.paymentType,
quoteId: missingQuote.quoteId,
recommendations: missingQuote.recommendations,
provider: missingQuote.provider,
isBuyAction: missingQuote.isBuyAction,
limits: missingQuote.limits,
);
quote.setFiatCurrency = missingQuote.fiatCurrency;
quote.setCryptoCurrency = missingQuote.cryptoCurrency;
return quote;
}).toList();
final updatedQuoteOptions = List<SelectableItem>.from([
OptionTitle(title: 'Recommended'),
...sortedRecommendedQuotes,
if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'),
...sortedQuotes,
if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'),
...outOfLimitQuotes,
]);
if (context.mounted) {
await Navigator.of(context).pushNamed(
Routes.buyOptionsPage,
arguments: [
updatedQuoteOptions,
changeOption,
launchTrade,
],
).then((value) => calculateBestRate());
}
}
void _onPairChange() {
_initialize();
}
void _setProviders() =>
providerList = isBuyAction ? availableBuyProviders : availableSellProviders;
providerList = mode == BuySellPageMode.buy ? availableBuyProviders : availableSellProviders;
Future<void> _initialize() async {
_setProviders();
@@ -343,53 +416,54 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress();
paymentMethodState = InitialPaymentMethod();
buySellQuotState = InitialBuySellQuotState();
sortedRecommendedQuotes.clear();
sortedQuotes.clear();
paymentMethods.clear();
await _getAvailablePaymentTypes();
await calculateBestRate();
}
String _getInitialCryptoCurrencyAddress() {
if (cryptoCurrency == wallet.currency) {
if ([CryptoCurrency.zec, CryptoCurrency.btc].contains(cryptoCurrency)) {
return wallet.walletAddresses.addressForBuy;
}
return wallet.walletAddresses.address;
}
return '';
}
String _getInitialCryptoCurrencyAddress() => wallet.walletAddresses.addressForBuy;
@action
Future<void> _getAvailablePaymentTypes() async {
paymentMethodState = PaymentMethodLoading();
selectedPaymentMethod = null;
final result = await Future.wait(providerList.map((element) =>
element.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction).timeout(
Duration(seconds: 10),
onTimeout: () => [],
)));
try{
paymentMethodState = PaymentMethodLoading();
selectedPaymentMethod = null;
final result = await Future.wait(providerList.map((element) => element
.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, mode == BuySellPageMode.buy)
.timeout(
Duration(seconds: 10),
onTimeout: () => [],
)));
final List<PaymentMethod> tempPaymentMethods = [];
final List<PaymentMethod> tempPaymentMethods = [];
for (var methods in result) {
for (var method in methods) {
final alreadyExists = tempPaymentMethods.any((m) {
return m.paymentMethodType == method.paymentMethodType;
});
for (var methods in result) {
for (var method in methods) {
final alreadyExists = tempPaymentMethods.any((m) {
if (m.paymentMethodType != method.paymentMethodType) return false;
if (method.paymentMethodType != PaymentType.unknown) return true;
return m.customTitle == method.customTitle;
});
if (!alreadyExists) {
tempPaymentMethods.add(method);
if (!alreadyExists) {
tempPaymentMethods.add(method);
}
}
}
}
paymentMethods = ObservableList<PaymentMethod>.of(tempPaymentMethods);
paymentMethods = ObservableList<PaymentMethod>.of(tempPaymentMethods);
if (paymentMethods.isNotEmpty) {
paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first;
selectedPaymentMethod!.isSelected = true;
paymentMethodState = PaymentMethodLoaded();
} else {
if (paymentMethods.isNotEmpty) {
paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first;
selectedPaymentMethod!.isSelected = true;
paymentMethodState = PaymentMethodLoaded();
} else {
paymentMethodState = PaymentMethodFailed();
}
} catch(e) {
paymentMethodState = PaymentMethodFailed();
}
}
@@ -399,17 +473,22 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
buySellQuotState = BuySellQuotLoading();
final List<BuyProvider> validProviders = providerList.where((provider) {
if (isBuyAction) {
return provider.supportedCryptoList
.any((pair) => pair.from == cryptoCurrency && pair.to == fiatCurrency);
if (mode == BuySellPageMode.buy) {
return provider.supportedCryptoList.any((pair) =>
pair.from.symbol == cryptoCurrency.symbol &&
pair.from.tag == cryptoCurrency.tag &&
pair.to.symbol == fiatCurrency.symbol);
} else {
return provider.supportedFiatList
.any((pair) => pair.from == fiatCurrency && pair.to == cryptoCurrency);
return provider.supportedFiatList.any((pair) =>
pair.from.symbol == fiatCurrency.symbol &&
pair.to.symbol == cryptoCurrency.symbol &&
pair.to.tag == cryptoCurrency.tag);
}
}).toList();
if (validProviders.isEmpty) {
buySellQuotState = BuySellQuotFailed();
buySellQuotState = BuySellQuotFailed(
errorMessage: S.current.no_buy_providers(cryptoCurrency.fullName??cryptoCurrency.title));
return;
}
@@ -419,7 +498,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
fiatCurrency: fiatCurrency,
amount: amount,
paymentType: selectedPaymentMethod?.paymentMethodType,
isBuyAction: isBuyAction,
isBuyAction: mode == BuySellPageMode.buy,
walletAddress: wallet.walletAddresses.address,
customPaymentMethodType: selectedPaymentMethod?.customPaymentMethodType,
)
@@ -437,11 +516,12 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
.toList();
if (validQuotes.isEmpty) {
buySellQuotState = BuySellQuotFailed();
buySellQuotState = BuySellQuotFailed(
errorMessage: S.current.no_buy_providers(cryptoCurrency.fullName??cryptoCurrency.title));
return;
}
if (isBuyAction) {
if (mode == BuySellPageMode.buy) {
validQuotes.sort((a, b) => b.payout.compareTo(a.payout));
} else {
validQuotes.sort((a, b) => a.payout.compareTo(b.payout));
@@ -464,7 +544,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
bestRateQuote = sortedRecommendedQuotes.first;
final Quote effectiveBestRateQuote = sortedRecommendedQuotes.reduce((a, b) {
return isBuyAction
return mode == BuySellPageMode.buy
? a.rate < b.rate
? a
: b
@@ -490,7 +570,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
context: context,
quote: selectedQuote!,
amount: amount,
isBuyAction: isBuyAction,
isBuyAction: mode == BuySellPageMode.buy,
cryptoCurrencyAddress: cryptoCurrencyAddress,
);
} catch (e) {
-91
View File
@@ -1,91 +0,0 @@
import 'package:hash_wallet/buy/buy_provider.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/view_model/buy/buy_item.dart';
import 'package:hive/hive.dart';
import 'package:hash_wallet/order/order.dart';
import 'package:hash_wallet/store/dashboard/orders_store.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'buy_amount_view_model.dart';
part 'buy_view_model.g.dart';
class BuyViewModel = BuyViewModelBase with _$BuyViewModel;
abstract class BuyViewModelBase with Store {
BuyViewModelBase(this.ordersSource, this.ordersStore, this.settingsStore, this.buyAmountViewModel,
{required this.wallet})
: isRunning = false,
isDisabled = true,
isShowProviderButtons = false,
items = <BuyItem>[] {
_fetchBuyItems();
}
final Box<Order> ordersSource;
final OrdersStore ordersStore;
final SettingsStore settingsStore;
final BuyAmountViewModel buyAmountViewModel;
final WalletBase wallet;
@observable
BuyProvider? selectedProvider;
@observable
List<BuyItem> items;
@observable
bool isRunning;
@observable
bool isDisabled;
@observable
bool isShowProviderButtons;
WalletType get type => wallet.type;
double get doubleAmount => buyAmountViewModel.doubleAmount;
@computed
FiatCurrency get fiatCurrency => buyAmountViewModel.fiatCurrency;
CryptoCurrency get cryptoCurrency => wallet.currency;
Future<String> fetchUrl() async {
String _url = '';
try {
_url = await selectedProvider!.requestUrl(doubleAmount.toString(), fiatCurrency.title);
} catch (e) {
printV(e.toString());
}
return _url;
}
Future<void> saveOrder(String orderId) async {
try {
final order = await selectedProvider!.findOrderById(orderId);
order.from = fiatCurrency.title;
order.to = cryptoCurrency.title;
await ordersSource.add(order);
ordersStore.setOrder(order);
} catch (e) {
printV(e.toString());
}
}
void reset() {
buyAmountViewModel.amount = '';
selectedProvider = null;
}
Future<void> _fetchBuyItems() async {
items = const <BuyItem>[];
}
}
@@ -14,6 +14,7 @@ import 'package:hash_wallet/utils/mobx.dart';
import 'package:hash_wallet/utils/token_utilities.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import "package:cw_core/utils/print_verbose.dart";
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
@@ -33,75 +34,100 @@ abstract class ContactListViewModelBase with Store {
}
Future<void> _init() async {
final walletInfos = await WalletInfo.getAll();
for (final info in walletInfos) {
final addressInfos = await info.getAddressInfos();
final addresses = await info.getAddresses();
if ([WalletType.monero, WalletType.wownero].contains(info.type) && addressInfos.isNotEmpty) {
for (var key in addressInfos.keys) {
final value = addressInfos[key];
final address = value?.first;
if (address != null) {
final name = _createName(info.name, address.label, key: key);
walletContacts.add(WalletContact(
address.address,
await loadWalletContacts();
_subscription = contactSource.bindToListWithTransform(
contacts,
(contact) => ContactRecord(contactSource, contact),
initialFire: true,
);
setOrderType(settingsStore.contactListOrder);
}
Future<void> _lastWalletContactsLoad = Future.value();
@action
Future<void> loadWalletContacts() async {
final waitFor = _lastWalletContactsLoad;
final done = Completer<void>();
_lastWalletContactsLoad = done.future;
await waitFor;
try {
final loadedContacts = <WalletContact>[];
final walletInfos = await WalletInfo.getAll();
for (final info in walletInfos) {
final addressInfos = await info.getAddressInfos();
final addresses = await info.getAddresses();
if ([WalletType.monero, WalletType.wownero].contains(info.type) &&
addressInfos.isNotEmpty) {
for (var key in addressInfos.keys) {
final value = addressInfos[key];
final address = value?.first;
if (address != null) {
final name = _createName(info.name, address.label, key: key);
loadedContacts.add(WalletContact(
address.address,
name,
getCryptoCurrencyForWalletListItem(
info.type,
),
walletType: info.type,
));
}
}
} else if (addresses.isNotEmpty == true && addresses.length > 1) {
if ([WalletType.monero, WalletType.wownero].contains(info.type)) {
final address = info.address;
final name = _createName(info.name, "", key: 0);
loadedContacts.add(WalletContact(
address,
name,
getCryptoCurrencyForWalletListItem(
info.type,
),
walletType: info.type,
));
} else {
addresses.forEach((address, label) {
if (label.isEmpty) {
return;
}
final name = _createName(info.name, label, key: null);
loadedContacts.add(WalletContact(
address,
name,
getCryptoCurrencyForWalletListItem(
info.type,
isTestnet: info.network == null
? false
: info.network!.toLowerCase().contains("testnet"),
),
walletType: info.type,
));
});
}
}
} else if (addresses.isNotEmpty == true && addresses.length > 1) {
if ([WalletType.monero, WalletType.wownero].contains(info.type)) {
final address = info.address;
final name = _createName(info.name, "", key: 0);
walletContacts.add(WalletContact(
address,
name,
} else {
loadedContacts.add(WalletContact(
info.address,
_createName(info.name, "",
key: [WalletType.monero, WalletType.wownero].contains(info.type) ? 0 : null),
getCryptoCurrencyForWalletListItem(
info.type,
),
walletType: info.type,
));
} else {
addresses.forEach((address, label) {
if (label.isEmpty) {
return;
}
final name = _createName(info.name, label, key: null);
walletContacts.add(WalletContact(
address,
name,
getCryptoCurrencyForWalletListItem(
info.type,
isTestnet:
info.network == null ? false : info.network!.toLowerCase().contains("testnet"),
),
walletType: info.type,
));
});
}
} else {
walletContacts.add(WalletContact(
info.address,
_createName(info.name, "",
key: [WalletType.monero, WalletType.wownero].contains(info.type) ? 0 : null),
getCryptoCurrencyForWalletListItem(
info.type,
),
walletType: info.type,
));
}
walletContacts = loadedContacts;
} catch (e) {
printV("loadWalletContacts failed: $e");
} finally {
done.complete();
}
_subscription = contactSource.bindToListWithTransform(
contacts, (Contact contact) => ContactRecord(contactSource, contact),
initialFire: true);
setOrderType(settingsStore.contactListOrder);
walletContacts = walletContacts.toList(); // rebuild
}
String _createName(String walletName, String label, {int? key = null}) {
@@ -1,7 +1,10 @@
import 'package:hash_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_wallet/di.dart';
import 'package:hash_wallet/entities/contact_record.dart';
import 'package:hash_wallet/entities/parse_address_from_domain.dart';
import 'package:hash_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:hash_wallet/main.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
@@ -16,7 +19,7 @@ part 'contact_view_model.g.dart';
class ContactViewModel = ContactViewModelBase with _$ContactViewModel;
abstract class ContactViewModelBase with Store {
ContactViewModelBase(this._contacts, {ContactRecord? contact})
ContactViewModelBase(this._contacts, this.appStore, this.adrResService, {ContactRecord? contact})
: state = InitialExecutionState(),
currencies = CryptoCurrency.all,
_contact = contact,
@@ -26,6 +29,9 @@ abstract class ContactViewModelBase with Store {
currency = contact?.type,
lastChange = contact?.lastChange;
final AppStore appStore;
final AddressResolverService adrResService;
@observable
ExecutionState state;
@@ -55,16 +61,50 @@ abstract class ContactViewModelBase with Store {
void reset() {
address = '';
name = '';
displayName = '';
currency = null;
}
Future<void> extractParsedAddress(BuildContext context) async {
if (currency == null) return;
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, address, currency!);
if (parsedAddress.name.isNotEmpty) {
displayName = parsedAddress.name;
Future<ParsedAddress?> extractParsedAddress(BuildContext context) async {
final wallet = appStore.wallet;
final currentCurrency = currency;
final query = address.trim();
if (wallet == null) return null;
if (currentCurrency == null) return null;
if (query.isEmpty) return null;
// Check if the address is valid for the current currency (except for Zano, which can use handles as addresses)
if (currentCurrency != CryptoCurrency.zano) {
final isValidAddress = AddressValidator(type: currentCurrency).isValid(query);
if (isValidAddress) return null;
}
address = await extractAddressFromParsed(null, parsedAddress);
final parsedAddresses = await adrResService.resolve(
query: query,
wallet: wallet,
currency: currentCurrency,
);
if (parsedAddresses.isEmpty) return null;
final resolvedAddress = parsedAddresses.first.parsedAddressByCurrencyMap[currentCurrency];
if (resolvedAddress == null || resolvedAddress.isEmpty) return null;
return parsedAddresses.first;
}
@action
void applyParsedAddress(ParsedAddress parsedAddress) {
final currentCurrency = currency;
if (currentCurrency == null) return;
final resolvedAddress = parsedAddress.parsedAddressByCurrencyMap[currentCurrency];
if (resolvedAddress == null || resolvedAddress.isEmpty) return;
address = resolvedAddress;
displayName =
parsedAddress.profileName.isNotEmpty ? parsedAddress.profileName : parsedAddress.handle;
}
Future<void> save() async {
@@ -1,32 +0,0 @@
enum ActionListDisplayMode { transactions, trades }
int serializeActionlistDisplayModes(List<ActionListDisplayMode> modes) {
var i = 0;
for (final mode in modes) {
switch (mode) {
case ActionListDisplayMode.trades:
i += 1;
break;
case ActionListDisplayMode.transactions:
i += 10;
break;
}
}
return i;
}
List<ActionListDisplayMode> deserializeActionlistDisplayModes(int raw) {
final modes = <ActionListDisplayMode>[];
if (raw == 1 || raw - 10 == 1) {
modes.add(ActionListDisplayMode.trades);
}
if (raw >= 10) {
modes.add(ActionListDisplayMode.transactions);
}
return modes;
}
+124 -91
View File
@@ -3,10 +3,13 @@ import 'package:hash_wallet/core/utilities.dart';
import 'package:hash_wallet/entities/balance_display_mode.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
import 'package:hash_wallet/entities/fiat_api_mode.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/entities/sort_balance_types.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/wallet_base.dart';
@@ -25,47 +28,74 @@ part 'balance_view_model.g.dart';
class BalanceRecord {
const BalanceRecord({
required this.raw,
required this.availableBalance,
required this.additionalBalance,
required this.secondAvailableBalance,
required this.secondAdditionalBalance,
required this.frozenBalance,
required this.fiatAvailableBalance,
required this.fiatAdditionalBalance,
required this.fiatFrozenBalance,
required this.fiatSecondAvailableBalance,
required this.fiatSecondAdditionalBalance,
required this.fiatAvailableBalanceRaw,
required this.fiatAdditionalBalanceRaw,
required this.fiatFrozenBalanceRaw,
required this.fiatSecondAvailableBalanceRaw,
required this.fiatSecondAdditionalBalanceRaw,
required this.asset,
required this.secondAsset,
required this.fiatCurrency,
required this.formattedAssetTitle,
this.languageCode,
});
final String fiatAdditionalBalance;
final String fiatAvailableBalance;
final String fiatFrozenBalance;
final Balance raw;
final String fiatAvailableBalanceRaw;
final String fiatAdditionalBalanceRaw;
final String fiatFrozenBalanceRaw;
final String fiatSecondAvailableBalanceRaw;
final String fiatSecondAdditionalBalanceRaw;
final String additionalBalance;
final String availableBalance;
final String frozenBalance;
final String secondAvailableBalance;
final String secondAdditionalBalance;
final String fiatSecondAdditionalBalance;
final String fiatSecondAvailableBalance;
final CryptoCurrency asset;
final CryptoCurrency secondAsset;
final FiatCurrency? fiatCurrency;
final String formattedAssetTitle;
final String? languageCode;
String get combinedAvailableBalance =>
((double.tryParse(availableBalance) ?? 0) + (double.tryParse(secondAvailableBalance) ?? 0))
(raw.available + (raw.secondAvailable ?? Money.zero(raw.available.currency)))
.toString()
.withMaxDecimals(8);
String get combinedFiatAvailableBalance => ((double.tryParse(fiatAvailableBalanceRaw) ?? 0) +
(double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0))
.toStringAsFixed(2);
String get combinedFiatAvailableBalance => fiatCurrency != null
? "$fiatCurrency " +
_withLocalSeperator(((double.tryParse(fiatAvailableBalanceRaw) ?? 0) +
(double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0))
.toStringAsFixed(2))
: "";
String get fiatAvailableBalanceRaw => fiatAvailableBalance.split(" ").last;
String get fiatSecondAvailableBalanceRaw => fiatSecondAvailableBalance.split(" ").last;
String get fiatCurrencyTicker => fiatAvailableBalance.split(" ").first;
String get fiatAvailableBalance =>
fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAvailableBalanceRaw)}" : "";
String get fiatAdditionalBalance =>
fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAdditionalBalanceRaw)}" : "";
String get fiatFrozenBalance =>
fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatFrozenBalanceRaw)}" : "";
String get fiatSecondAvailableBalance => fiatCurrency != null
? "$fiatCurrency ${_withLocalSeperator(fiatSecondAvailableBalanceRaw)}"
: "";
String get fiatSecondAdditionalBalance => fiatCurrency != null
? "$fiatCurrency ${_withLocalSeperator(fiatSecondAdditionalBalanceRaw)}"
: "";
String _withLocalSeperator(String rawAmount) =>
languageCode == null ? rawAmount : rawAmount.withLocalSeperator(languageCode);
}
class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel;
@@ -125,7 +155,7 @@ abstract class BalanceViewModelBase with Store {
bool get isEVMCompatible => isEVMCompatibleChain(wallet.type);
@computed
bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type);
bool get hasAccounts => wallet.hasAccountsSupport;
@computed
SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy;
@@ -208,13 +238,13 @@ abstract class BalanceViewModelBase with Store {
}
}
String additionalBalance(CryptoCurrency cryptoCurrency) {
Money additionalBalance(CryptoCurrency cryptoCurrency) {
final balance = _currencyBalance(cryptoCurrency);
if (displayMode == BalanceDisplayMode.hiddenBalance || balance.additional == BigInt.zero)
return '0.0';
if (displayMode == BalanceDisplayMode.hiddenBalance || balance.unavailable.isZero)
return Money.zero(cryptoCurrency);
return cryptoCurrency.formatAmount(balance.additional);
return balance.unavailable;
}
@computed
@@ -225,24 +255,26 @@ abstract class BalanceViewModelBase with Store {
if (displayMode == BalanceDisplayMode.hiddenBalance) {
final fiatCurrency = settingsStore.fiatCurrency;
return MapEntry(
key,
BalanceRecord(
availableBalance: '●●●●●●',
additionalBalance: additionalBalance(key),
frozenBalance: '',
secondAvailableBalance: '●●●●●●',
secondAdditionalBalance: '●●●●●●',
fiatAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
fiatAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
fiatFrozenBalance: isFiatDisabled ? '' : '',
fiatSecondAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()}',
fiatSecondAdditionalBalance:
isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
asset: key,
secondAsset: secondAsset,
formattedAssetTitle: _formatterAsset(key)));
key,
BalanceRecord(
raw: value,
availableBalance: '●●●●●●',
additionalBalance: '',
frozenBalance: '',
secondAvailableBalance: '●●●●●●',
secondAdditionalBalance: '●●●●●●',
fiatAdditionalBalanceRaw: '●●●●●',
fiatAvailableBalanceRaw: '●●●●●',
fiatFrozenBalanceRaw: '',
fiatSecondAvailableBalanceRaw: '●●●●●',
fiatSecondAdditionalBalanceRaw: '●●●●●',
asset: key,
secondAsset: secondAsset,
fiatCurrency: isFiatDisabled ? null : fiatCurrency,
formattedAssetTitle: _formatterAsset(key),
),
);
}
final fiatCurrency = settingsStore.fiatCurrency;
final price = key.isPotentialScam
? 0.0
: fiatConversionStore.prices[key == CryptoCurrency.btcln ? CryptoCurrency.btc : key] ?? 0;
@@ -251,51 +283,41 @@ abstract class BalanceViewModelBase with Store {
// throw Exception('Price is null for: $key');
// }
final available = evm?.getERC20AvailableBalance(value) ??
(value.fullAvailableBalance - (value.secondAvailable ?? BigInt.zero));
final additional = evm?.getERC20AvailableBalance(value) ??
(value.additional - (value.secondAdditional ?? BigInt.zero));
final availableFiatBalance =
isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.available);
final availableFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(available))}';
final additionalFiatBalance =
isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.unavailable);
final additionalFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(additional))}';
final frozenFiatBalance =
isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.frozen);
final frozenFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.frozen != null ? key.formatAmount(value.frozen!) : null)}';
final secondAvailableFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAvailable != null ? key.formatAmount(value.secondAvailable!) : null)}';
final secondAvailableFiatBalance =
isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.secondAvailable);
final secondAdditionalFiatBalance = isFiatDisabled
? ''
: '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAdditional != null ? key.formatAmount(value.secondAdditional!) : null)}';
: _getFiatBalance(price: price, cryptoAmount: value.secondUnavailable);
return MapEntry(
key,
BalanceRecord(
availableBalance: _getFormattedCryptoAmount(key, available),
fiatAvailableBalance: availableFiatBalance,
additionalBalance: _getFormattedCryptoAmount(key, additional),
fiatAdditionalBalance: additionalFiatBalance,
frozenBalance: (value.frozen ?? BigInt.zero) > BigInt.zero
? _getFormattedCryptoAmount(key, value.frozen ?? BigInt.zero)
: '',
fiatFrozenBalance: frozenFiatBalance,
secondAvailableBalance:
_getFormattedCryptoAmount(secondAsset, value.secondAvailable ?? BigInt.zero),
fiatSecondAvailableBalance: secondAvailableFiatBalance,
secondAdditionalBalance:
_getFormattedCryptoAmount(secondAsset, value.secondAdditional ?? BigInt.zero),
fiatSecondAdditionalBalance: secondAdditionalFiatBalance,
raw: value,
availableBalance: _getFormattedCryptoAmount(value.available),
fiatAvailableBalanceRaw: availableFiatBalance,
additionalBalance: _getFormattedCryptoAmount(value.unavailable),
fiatAdditionalBalanceRaw: additionalFiatBalance,
frozenBalance: _getFormattedCryptoAmount(value.frozen),
fiatFrozenBalanceRaw: frozenFiatBalance,
secondAvailableBalance: _getFormattedCryptoAmount(value.secondAvailable),
fiatSecondAvailableBalanceRaw: secondAvailableFiatBalance,
secondAdditionalBalance: _getFormattedCryptoAmount(value.secondUnavailable),
fiatSecondAdditionalBalanceRaw: secondAdditionalFiatBalance,
asset: key,
secondAsset: secondAsset,
fiatCurrency: isFiatDisabled ? null : settingsStore.fiatCurrency,
formattedAssetTitle: _formatterAsset(key),
languageCode: settingsStore.languageCode,
),
);
});
@@ -306,7 +328,7 @@ abstract class BalanceViewModelBase with Store {
bool hasAdditionalBalance(CryptoCurrency currency) {
final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type);
final isNotZeroAmount = additionalBalance(currency) != "0.0";
final isNotZeroAmount = !additionalBalance(currency).isZero;
return isWalletTypeActivated && isNotZeroAmount;
}
@@ -314,9 +336,9 @@ abstract class BalanceViewModelBase with Store {
@computed
bool get hasSecondAdditionalBalance {
if (wallet.type == WalletType.litecoin && mwebEnabled) {
return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0;
return (wallet.balance[CryptoCurrency.ltc]?.secondUnavailable ?? 0) != 0;
} else if (wallet.type == WalletType.bitcoin) {
return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0;
return (wallet.balance[CryptoCurrency.btc]?.secondUnavailable ?? 0) != 0;
}
return false;
}
@@ -338,12 +360,11 @@ abstract class BalanceViewModelBase with Store {
WalletType.wownero,
].contains(type);
String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, BigInt? amount) {
String _getFormattedCryptoAmount(Money? amount) {
if (amount == null) return "";
return appStore.amountParsingProxy
.getDisplayCryptoStringFromBigInt(amount, cryptoCurrency)
.withMaxDecimals(8)
.asDisplayString(amount)
.withLocalSeperator(settingsStore.languageCode);
}
@@ -390,9 +411,9 @@ abstract class BalanceViewModelBase with Store {
switch (sortBalanceBy) {
case SortBalanceBy.FiatBalance:
final aFiatBalance = _getFiatBalance(
price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance);
price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.raw.available);
final bFiatBalance = _getFiatBalance(
price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance);
price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.raw.available);
return (double.tryParse(bFiatBalance) ?? 0)
.compareTo((double.tryParse(aFiatBalance)) ?? 0);
@@ -416,10 +437,10 @@ abstract class BalanceViewModelBase with Store {
return formattedBalances.firstWhereOrNull((item) =>
(getTokenAddressBasedOnWallet(item.asset) ==
wallet.walletInfo.favoriteTokenAddress)) ??
formattedBalances.elementAt(0);
formattedBalances.elementAtOrNull(0);
}
return formattedBalances.elementAt(0);
return formattedBalances.elementAtOrNull(0);
}
String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
@@ -447,15 +468,13 @@ abstract class BalanceViewModelBase with Store {
double ret = 0.0;
for (final curr in wallet.balance.keys) {
final record = wallet.balance[curr]!;
final available = evm?.getERC20AvailableBalance(record) ??
(record.fullAvailableBalance - (record.secondAvailable ?? BigInt.zero));
final available = record.available - (record.secondAvailable ?? Money.zero(curr));
final price = fiatConversionStore.prices[curr] ?? 0;
ret += double.tryParse(calculateFiatAmount(
price: price, cryptoAmount: curr.formatAmount(available).replaceAll(",", ""))
ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString())
.replaceAll(",", "")) ??
0;
}
return ret.toStringAsFixed(2);
return ret.toStringAsFixed(2).withLocalSeperator(settingsStore.languageCode);
}
Balance _currencyBalance(CryptoCurrency cryptoCurrency) {
@@ -499,13 +518,27 @@ abstract class BalanceViewModelBase with Store {
}
}
String _getFiatBalance({required double price, String? cryptoAmount}) {
if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) {
return '0.00'.withLocalSeperator(settingsStore.languageCode);
String? accountFiatBalance(AccountListItem account) {
if (isFiatDisabled) {
return null;
}
return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount)
.withLocalSeperator(settingsStore.languageCode);
final fiat = settingsStore.fiatCurrency.title;
final value = savedDisplayMode == BalanceDisplayMode.hiddenBalance
? "●●●●●"
: calculateFiatAmount(
price: price,
cryptoAmount: account.balance.toString(),
).withLocalSeperator(settingsStore.languageCode);
return "$value $fiat";
}
String _getFiatBalance({required double price, Money? cryptoAmount}) {
if (cryptoAmount == null) {
return '0.00';
}
return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount.toString(), raw: true);
}
String _formatterAsset(CryptoCurrency asset) {
+345 -142
View File
@@ -5,6 +5,11 @@ import 'dart:io' show Platform;
import 'package:hash_wallet/.secrets.g.dart' as secrets;
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/core/key_service.dart';
import 'package:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:hash_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cw_core/account.dart';
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
import "package:cw_core/balance_card_layout.dart";
import "package:cw_core/balance_card_style_settings.dart";
import 'package:hash_wallet/core/trade_monitor.dart';
import 'package:hash_wallet/entities/auto_generate_subaddress_status.dart';
@@ -18,7 +23,6 @@ import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/utils/feature_flag.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/nano/nano.dart';
import 'package:hash_wallet/order/order_provider_description.dart';
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
import 'package:hash_wallet/store/dashboard/order_filter_store.dart';
import 'package:hash_wallet/utils/device_info.dart';
@@ -35,7 +39,6 @@ import 'package:hash_wallet/store/dashboard/trade_filter_store.dart';
import 'package:hash_wallet/store/dashboard/trades_store.dart';
import 'package:hash_wallet/store/dashboard/transaction_filter_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/store/yat/yat_store.dart';
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
@@ -54,7 +57,7 @@ import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
@@ -77,6 +80,7 @@ class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
abstract class DashboardViewModelBase with Store {
DashboardViewModelBase(
{required this.balanceViewModel,
required this.accountListViewModelFactory,
required this.tradeMonitor,
required this.appStore,
required this.tradesStore,
@@ -84,7 +88,6 @@ abstract class DashboardViewModelBase with Store {
required this.orderFilterStore,
required this.transactionFilterStore,
required this.settingsStore,
required this.yatStore,
required this.ordersStore,
required this.anonpayTransactionsStore,
required this.payjoinTransactionsStore,
@@ -95,49 +98,8 @@ abstract class DashboardViewModelBase with Store {
isShowFirstYatIntroduction = false,
isShowSecondYatIntroduction = false,
isShowThirdYatIntroduction = false,
filterItems = {
S.current.transactions: [
FilterItem(
value: () => transactionFilterStore.displayAll,
caption: S.current.all_transactions,
onChanged: transactionFilterStore.toggleAll),
FilterItem(
value: () => transactionFilterStore.displayIncoming,
caption: S.current.incoming,
onChanged: transactionFilterStore.toggleIncoming),
FilterItem(
value: () => transactionFilterStore.displayOutgoing,
caption: S.current.outgoing,
onChanged: transactionFilterStore.toggleOutgoing),
if (appStore.wallet!.type == WalletType.bitcoin)
FilterItem(
value: () => transactionFilterStore.displaySilentPayments,
caption: S.current.silent_payments,
onChanged: transactionFilterStore.toggleSilentPayments,
),
// FilterItem(
// value: () => false,
// caption: S.current.transactions_by_date,
// onChanged: null),
],
// Orders (Cake Pay) section removed — Hash Bags does not integrate
// Cake Pay. MoonPay buy orders are source:buy and have no filter row.
S.current.trades: [
FilterItem(
value: () => tradeFilterStore.displayAllTrades,
caption: S.current.all_trades,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
// Only Trocador is enabled as a swap provider (see
// exchange_view_model.dart _allProviders); other providers are
// disabled pending affiliate keys, so they are omitted here too.
FilterItem(
value: () => tradeFilterStore.displayTrocador,
caption: ExchangeProviderDescription.trocador.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
]
},
filterItems = [],
exchangeFilterItems = [],
subname = '',
name = appStore.wallet!.name,
type = appStore.wallet!.type,
@@ -146,6 +108,9 @@ abstract class DashboardViewModelBase with Store {
cardOrder = ObservableMap<int, int>(),
wallet = appStore.wallet! {
showDecredInfoCard = false;
showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
isMultiAccountsEnabled =
wallet.hasNativeAccounts || wallet.walletInfo.isMultiAccountsEnabled == true;
name = wallet.name;
type = wallet.type;
@@ -155,13 +120,15 @@ abstract class DashboardViewModelBase with Store {
unawaited(isBackgroundSyncEnabled());
unawaited(isBatteryOptimizationEnabled());
unawaited(_loadConstraints());
accountListViewModel = accountListViewModelFactory();
final _wallet = wallet;
loadFilterItems();
if (_wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
_onAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction(
@@ -189,9 +156,9 @@ abstract class DashboardViewModelBase with Store {
} else if (_wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction(
_onAccountChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
(wow.Account account) => _onMoneroAccountChange(_wallet));
(Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
@@ -217,21 +184,12 @@ abstract class DashboardViewModelBase with Store {
),
);
} else {
final sortedTransactions = [...wallet.transactionHistory.transactions.values];
sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
transactions = ObservableList.of(
sortedTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
appStore: appStore,
key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'),
),
),
);
subname = "";
_reloadTransactions();
}
_setupBitcoinAccountChangeReaction(_wallet);
// TODO: nano sub-account generation is disabled:
// if (_wallet.type == WalletType.nano || _wallet.type == WalletType.banano) {
// subname = nano!.getCurrentAccount(_wallet).label;
@@ -239,10 +197,13 @@ abstract class DashboardViewModelBase with Store {
_walletChangeDisposer?.reaction.dispose();
_walletChangeDisposer = reaction((_) => appStore.wallet, (wallet) {
accountListViewModel = accountListViewModelFactory();
_onWalletChange(wallet);
resetLightningMode();
_checkMweb();
loadCardDesigns();
showDecredInfoCard = false;
loadSeedBackupReminder();
tradeMonitor.stopTradeMonitoring();
tradeMonitor.monitorActiveTrades(wallet!.id);
@@ -282,6 +243,42 @@ abstract class DashboardViewModelBase with Store {
tradeMonitor.monitorActiveTrades(wallet.id);
}
void loadFilterItems() {
filterItems = [
// FilterItem(
// value: () => transactionFilterStore.displayAll,
// caption: S.current.all_transactions,
// onChanged: transactionFilterStore.toggleAll),
FilterItem(
value: () => transactionFilterStore.displayOutgoing,
caption: S.current.send,
onChanged: transactionFilterStore.toggleOutgoing),
FilterItem(
value: () => transactionFilterStore.displayIncoming,
caption: S.current.receive,
onChanged: transactionFilterStore.toggleIncoming),
if (appStore.wallet!.type == WalletType.bitcoin)
FilterItem(
value: () => transactionFilterStore.displaySilentPayments,
caption: S.current.silent_payments,
onChanged: transactionFilterStore.toggleSilentPayments,
),
SwapFilterItem(
enabledProviders: () => tradeFilterStore.enabledProvidersCount,
allEnabled: () => tradeFilterStore.displayAllTrades,
value: () => tradeFilterStore.enabledProvidersCount > 0,
onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
];
// Hash Bags: Trocador is the only swap provider.
exchangeFilterItems = [
SwapProviderFilterItem(
providerDescription: ExchangeProviderDescription.trocador,
value: () => tradeFilterStore.displayTrocador,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
];
}
bool _isTransactionDisposerCallbackRunning = false;
@action
@@ -292,8 +289,14 @@ abstract class DashboardViewModelBase with Store {
transactions.clear();
final filteredTransactions = wallet.type == WalletType.bitcoin
? bitcoin!.getCurrentAccountBitcoinTransactions(wallet)
: wallet.transactionHistory.transactions.values.toList();
filteredTransactions.sort((a, b) => a.date.compareTo(b.date));
transactions.addAll(
wallet.transactionHistory.transactions.values.map(
filteredTransactions.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
@@ -304,6 +307,10 @@ abstract class DashboardViewModelBase with Store {
);
}
@computed
// Hash Bags: no Zcash, so never migrating.
bool get isMigratingToIronwood => false;
@computed
bool get isSyncHeavy {
if ([
@@ -333,64 +340,99 @@ abstract class DashboardViewModelBase with Store {
}
@action
Future<void> loadCardDesigns() async {
final accountStyleSettings =
await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
void changeAllFilterItems(bool value) {
for (final item in filterItems) {
if (item.value() != value) {
item.onChanged();
}
}
for (final item in exchangeFilterItems) {
if (item.value() != value) {
item.onChanged();
}
}
}
late final int numAccounts;
List<int> get cardAccountIndices {
if (wallet.type == WalletType.monero) {
numAccounts = monero!.getAccountList(wallet).accounts.length;
} else if (wallet.type == WalletType.wownero) {
numAccounts = wow.wownero!.getAccountList(wallet).accounts.length;
} else if (wallet.type == WalletType.bitcoin) {
// bitcoin and lightning
numAccounts = 2;
} else {
numAccounts = 1;
return monero!.getAccountList(wallet).accounts.map((account) => account.id).toList();
}
cardDesigns.clear();
Map<int, int> newOrder = {};
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getAccountList(wallet).accounts.map((account) => account.id).toList();
}
if (wallet.type == WalletType.bitcoin) {
final accounts = accountListViewModel?.accounts;
return [
if (accounts == null || accounts.isEmpty) 0 else ...accounts.map((account) => account.id),
-2,
];
}
return const [-1];
}
for (int i = 0; i < numAccounts; i++) {
late final int index;
if (balanceViewModel.hasAccounts) {
index = i;
} else if (wallet.type == WalletType.bitcoin && i == 1) {
index = 0;
} else {
index = -1;
}
final setting = accountStyleSettings.where((e) => e.accountIndex == index).firstOrNull;
late final CryptoCurrency curr;
if (wallet.type == WalletType.bitcoin && i == 1) {
curr = CryptoCurrency.btcln;
} else {
curr = wallet.currency;
}
cardDesigns.add(CardDesign.fromStyleSettings(setting, curr));
if (setting?.cardOrder != null) {
newOrder[setting!.cardOrder] = i;
@action
Future<void> loadCardDesigns() async {
if (wallet.type == WalletType.bitcoin) {
await accountListViewModel?.reload();
}
final accountIndices = cardAccountIndices;
final settings = await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
if (wallet.type != WalletType.bitcoin && accountIndices.contains(0) &&
!settings.any((setting) => setting.accountIndex == 0)) {
final primarySettings = settings.where((setting) => setting.accountIndex == -1)
.firstOrNull?.copyWith(accountIndex: 0);
if (primarySettings != null) {
await primarySettings.insert();
settings.add(primarySettings);
}
}
final layout = BalanceCardLayout.resolve(
accountIndices: accountIndices.where((index) => index != -2).toList(),
settings: settings,
);
// making sure ALL accounts have numbers, even the ones that existed before this feature was a thing
for (int i = 0; i < numAccounts; i++) {
if (!newOrder.containsKey(i) && !(wallet.type != WalletType.bitcoin && i == 1)) {
int free = 0;
while (newOrder.containsValue(free)) {
free++;
}
if (wallet.type == WalletType.bitcoin) {
newOrder[free] = 0;
} else {
newOrder[free] = i;
}
cardDesigns
..clear()
..addAll(accountIndices.map((accountIndex) => CardDesign.fromStyleSettings(
settings.where((setting) => setting.accountIndex == accountIndex).firstOrNull,
accountIndex == -2 ? CryptoCurrency.btcln : wallet.currency,
)));
cardOrder = {
for (int position = 0; position < layout.visible.length; position++)
position: layout.visible[position],
}.asObservable();
if (layout.needsRepair) {
await BalanceCardStyleSettings.setVisibleOrder(wallet.walletInfo.internalId, layout.orders);
}
if (isMultiAccountsEnabled && !lightningMode && layout.visible.isNotEmpty &&
!layout.visible.contains(accountListViewModel?.selectedAccount?.id)) {
final visibleAccount = accountListViewModel?.accounts
.where((account) => account.id == layout.visible.last).firstOrNull;
if (visibleAccount != null) {
await accountListViewModel!.select(visibleAccount);
}
}
cardOrder = newOrder.asObservable();
}
int get currentCardAccountIndex {
if (wallet.type == WalletType.monero) {
return monero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.wownero) {
return wow.wownero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.bitcoin) {
return lightningMode ? -2 : wallet.walletInfo.currentAccountIndex;
}
return -1;
}
CardDesign get currentCardDesign {
final designIndex = cardAccountIndices.indexOf(currentCardAccountIndex);
return cardDesigns.isEmpty || designIndex >= cardDesigns.length
? CardDesign.fromStyleSettings(null, wallet.currency)
: cardDesigns[designIndex == -1 ? 0 : designIndex];
}
void _transactionDisposerCallback(int _) async {
@@ -405,36 +447,43 @@ abstract class DashboardViewModelBase with Store {
: wallet.type == WalletType.wownero
? wow.wownero!.getCurrentAccount(wallet).id
: null;
final List<TransactionInfo> relevantTxs = [];
for (final tx in appStore.wallet!.transactionHistory.transactions.values) {
bool isRelevant = true;
if (wallet.type == WalletType.monero) {
isRelevant = monero!.getTransactionInfoAccountId(tx) == currentAccountId;
} else if (wallet.type == WalletType.wownero) {
isRelevant = wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId;
}
final allTxs = appStore.wallet!.transactionHistory.transactions.values;
final List<TransactionInfo> relevantTxs;
if (isRelevant) {
relevantTxs.add(tx);
}
if (wallet.type == WalletType.monero) {
relevantTxs = allTxs
.where((tx) => monero!.getTransactionInfoAccountId(tx) == currentAccountId)
.toList();
} else if (wallet.type == WalletType.wownero) {
relevantTxs = allTxs
.where((tx) => wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId)
.toList();
} else if (wallet.type == WalletType.bitcoin) {
relevantTxs = bitcoin!.getCurrentAccountBitcoinTransactions(wallet);
} else {
relevantTxs = allTxs.toList();
}
// printV("Transaction disposer callback (relevantTxs: ${relevantTxs.length} current: ${transactions.length})");
// TODO(malik) update this in a saner way during the vm refactor
String _txIdentityString(String txHash, TransactionDirection direction) =>
"${txHash}_$direction";
String _txIdentityStringConfirmations(
String txHash, TransactionDirection direction, int confirmations) =>
"${txHash}_${direction}_$confirmations";
String txHash, TransactionDirection direction, int confirmations, bool isPending) =>
"${txHash}_${direction}_${confirmations}_$isPending";
final existingKeys = transactions
.map((item) => _txIdentityStringConfirmations(
item.transaction.txHash, item.transaction.direction, item.transaction.confirmations))
item.transaction.txHash,
item.transaction.direction,
item.transaction.confirmations,
item.transaction.isPending))
.toSet();
final newTransactions = relevantTxs
.where((tx) => !existingKeys
.contains(_txIdentityStringConfirmations(tx.txHash, tx.direction, tx.confirmations)))
.where((tx) => !existingKeys.contains(_txIdentityStringConfirmations(
tx.txHash, tx.direction, tx.confirmations, tx.isPending)))
.map((tx) => TransactionListItem(
transaction: tx,
balanceViewModel: balanceViewModel,
@@ -499,6 +548,12 @@ abstract class DashboardViewModelBase with Store {
@observable
ObservableMap<int, int> cardOrder;
@observable
bool lightningMode = false;
@observable
WalletAccountListViewModel? accountListViewModel;
@computed
bool get isDarkTheme => appStore.themeStore.currentTheme.isDark;
@@ -513,13 +568,14 @@ abstract class DashboardViewModelBase with Store {
@computed
bool get shouldShowMwebAd {
return false;
if (wallet.type != WalletType.litecoin) return false;
if (mwebEnabled) return false;
if (settingsStore.mwebAdDismissed) return false;
return Platform.isAndroid || Platform.isIOS;
return (Platform.isAndroid || Platform.isIOS) && !wallet.isHardwareWallet;
}
@action
@@ -645,6 +701,12 @@ abstract class DashboardViewModelBase with Store {
return formattedItemsList(_items);
}
static const shortHistoryLength = 3;
@computed
List<ActionListItem> get itemsShort =>
items.where((item) => item is! DateSectionItem).take(shortHistoryLength).toList();
@observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
@@ -787,6 +849,60 @@ abstract class DashboardViewModelBase with Store {
@observable
late bool showDecredInfoCard;
@observable
late bool showSeedBackupReminder;
@computed
bool get canToggleMultiAccounts => wallet.canToggleMultiAccounts;
@observable
late bool isMultiAccountsEnabled;
@computed
List<AccountListItem> get visibleAccounts {
final all = accountListViewModel?.accounts;
if (all == null || all.isEmpty) return const <AccountListItem>[];
if (isMultiAccountsEnabled) {
return all.where((account) => cardOrder.containsValue(account.id)).toList(growable: false);
}
final primary = all.where((a) => a.id == 0).firstOrNull ?? all.first;
return <AccountListItem>[primary];
}
@action
Future<void> setMultiAccountsEnabled(bool value) async {
if (!canToggleMultiAccounts) {
return;
}
if (!value) {
await _selectPrimaryAccount();
}
wallet.walletInfo.isMultiAccountsEnabled = value;
await wallet.walletInfo.save();
isMultiAccountsEnabled = value;
await accountListViewModel?.reload();
await loadCardDesigns();
if (value) {
unawaited(wallet.startSync());
}
}
@computed
bool get hasBalance => wallet.balance.values.any(
(balance) =>
!balance.available.isZero ||
!balance.unavailable.isZero ||
!(balance.secondAvailable?.isZero ?? true) ||
!(balance.secondUnavailable?.isZero ?? true) ||
!(balance.frozen?.isZero ?? true),
);
@computed
bool get shouldShowSeedBackupReminder => showSeedBackupReminder && hasBalance;
@computed
bool get showPayjoinCard =>
wallet.type == WalletType.bitcoin &&
@@ -1011,6 +1127,17 @@ abstract class DashboardViewModelBase with Store {
sharedPreferences.setBool(PreferencesKey.showDecredInfoCard, false);
}
@action
void loadSeedBackupReminder() {
showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
}
@action
Future<void> dismissSeedBackupReminder() async {
showSeedBackupReminder = false;
await wallet.walletInfo.clearSeedBackupReminder();
}
@action
void dismissPayjoin() {
settingsStore.showPayjoinCard = false;
@@ -1023,6 +1150,8 @@ abstract class DashboardViewModelBase with Store {
bitcoin!.updatePayjoinState(wallet, true);
}
final WalletAccountListViewModel? Function() accountListViewModelFactory;
BalanceViewModel balanceViewModel;
TradeMonitor tradeMonitor;
@@ -1031,7 +1160,6 @@ abstract class DashboardViewModelBase with Store {
SettingsStore settingsStore;
YatStore yatStore;
TradesStore tradesStore;
@@ -1047,7 +1175,11 @@ abstract class DashboardViewModelBase with Store {
PayjoinTransactionsStore payjoinTransactionsStore;
Map<String, List<FilterItem>> filterItems;
// Map<String, List<FilterItem>> filterItems;
List<FilterItem> filterItems;
List<FilterItem> exchangeFilterItems;
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
@@ -1074,7 +1206,7 @@ abstract class DashboardViewModelBase with Store {
@computed
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
ReactionDisposer? _onMoneroAccountChangeReaction;
ReactionDisposer? _onAccountChangeReaction;
ReactionDisposer? _onMoneroBalanceChangeReaction;
@@ -1142,6 +1274,37 @@ abstract class DashboardViewModelBase with Store {
}
}
@action
void toggleLightningMode() {
lightningMode = !lightningMode;
// Lightning belongs to the primary account only
if (lightningMode) {
unawaited(_selectPrimaryAccount().then((_) async {
if (!lightningMode && isMultiAccountsEnabled) {
await loadCardDesigns();
}
}));
} else {
unawaited(loadCardDesigns());
}
}
Future<void> _selectPrimaryAccount() async {
if (wallet.type != WalletType.bitcoin) return;
final vm = accountListViewModel;
if (vm == null || vm.selectedAccount?.id == 0) return;
final primary = vm.accounts.where((a) => a.id == 0).firstOrNull;
if (primary != null) {
await vm.select(primary);
}
}
@action
void resetLightningMode() => lightningMode = false;
@action
void _onWalletChange(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
@@ -1152,15 +1315,20 @@ abstract class DashboardViewModelBase with Store {
this.wallet = wallet;
type = wallet.type;
name = wallet.name;
isMultiAccountsEnabled =
wallet.hasNativeAccounts || wallet.walletInfo.isMultiAccountsEnabled == true;
_onAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction = null;
loadFilterItems();
if (wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(wallet).label;
_onMoneroAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
_onAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction(
@@ -1171,12 +1339,12 @@ abstract class DashboardViewModelBase with Store {
} else if (wallet.type == WalletType.wownero) {
subname = wow.wownero!.getCurrentAccount(wallet).label;
_onMoneroAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction(
_onAccountChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
(wow.Account account) => _onMoneroAccountChange(wallet));
(Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction(
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
@@ -1187,7 +1355,7 @@ abstract class DashboardViewModelBase with Store {
// FIX-ME: Check for side effects
// subname = null;
subname = '';
_setupBitcoinAccountChangeReaction(wallet);
_reloadTransactions();
}
@@ -1223,6 +1391,21 @@ abstract class DashboardViewModelBase with Store {
}, _transactionDisposerCallback, delay: 300);
}
void _setupBitcoinAccountChangeReaction(WalletBase wallet) {
if (wallet.type != WalletType.bitcoin) {
return;
}
_onAccountChangeReaction?.reaction.dispose();
_onAccountChangeReaction = reaction(
(_) => accountListViewModel?.selectedAccount?.id,
(_) {
_reloadTransactions();
},
fireImmediately: true,
);
}
@action
void _onMoneroAccountChange(WalletBase wallet) {
if (wallet.type == WalletType.monero) {
@@ -1349,7 +1532,8 @@ abstract class DashboardViewModelBase with Store {
if (walletInfo.type == WalletType.bitcoin) {
final password = await keyService.getWalletPassword(walletName: walletInfo.name);
final path = await pathForWallet(name: walletInfo.name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password);
final encryption = encryptionFileUtilsFor(SettingsStoreBase.walletPasswordDirectInput);
final jsonSource = await encryption.read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
@@ -1406,4 +1590,23 @@ abstract class DashboardViewModelBase with Store {
Future<void> refreshDashboard() async {
reconnect();
}
// Hash Bags: Zano never shipped here, nothing to deprecate.
static const walletTypesToBeRemoved = <WalletType>[];
Future<bool> shouldShowRemovalPopup() async {
final show = (await WalletInfo.getAll()).any((item) =>
walletTypesToBeRemoved.contains(item.type) &&
!(sharedPreferences.getBool(PreferencesKey.deprecationPopupViewed(item.type)) ?? false),);
for (final type in walletTypesToBeRemoved) {
await sharedPreferences.setBool(PreferencesKey.deprecationPopupViewed(type), true);
}
// if user is actively using a wallet type that's about to be removed, keep nagging them
return show || walletTypesToBeRemoved.contains(wallet.type);
}
Future<List<WalletInfo>> get walletsToBeRemoved async => (await WalletInfo.getAll())
.where((item) => walletTypesToBeRemoved.contains(item.type))
.toList();
}
@@ -1,3 +1,4 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
class DateSectionItem extends ActionListItem {
@@ -6,3 +7,30 @@ class DateSectionItem extends ActionListItem {
@override
final DateTime date;
}
abstract class SpecificDateSectionItem extends DateSectionItem {
SpecificDateSectionItem(super.date, {required super.key});
String get text;
}
class Last30daysTransactionItem extends SpecificDateSectionItem {
Last30daysTransactionItem(super.date, {required super.key});
@override
String get text => S.current.last_30_days;
}
class Last7daysTransactionItem extends SpecificDateSectionItem {
Last7daysTransactionItem(super.date, {required super.key});
@override
String get text => S.current.last_7_days;
}
class TodayTransactionItem extends SpecificDateSectionItem {
TodayTransactionItem(super.date, {required super.key});
@override
String get text => S.current.today;
}
@@ -1,35 +0,0 @@
import 'package:mobx/mobx.dart';
part 'desktop_sidebar_view_model.g.dart';
enum SidebarItem {
dashboard,
transactions,
status,
support,
settings,
}
class DesktopSidebarViewModel = DesktopSidebarViewModelBase with _$DesktopSidebarViewModel;
abstract class DesktopSidebarViewModelBase with Store {
DesktopSidebarViewModelBase();
@observable
SidebarItem currentPage = SidebarItem.dashboard;
@action
void onPageChange(SidebarItem item) {
if (currentPage == item) {
resetSidebar();
return;
}
currentPage = item;
}
@action
void resetSidebar() {
currentPage = SidebarItem.dashboard;
}
}
@@ -1,19 +0,0 @@
import 'package:hash_wallet/view_model/dashboard/filter_item.dart';
class DropdownFilterItem extends FilterItem {
DropdownFilterItem({
required this.items,
required this.caption,
required this.selectedItem,
required this.onItemSelected,
}) : super(
value: () => false,
caption: caption,
onChanged: (_) {},
);
final List<String> items;
final String caption;
final String selectedItem;
final Function(String) onItemSelected;
}
+27 -1
View File
@@ -1,4 +1,4 @@
import 'package:mobx/mobx.dart';
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
class FilterItem {
FilterItem({required this.value, required this.caption, required this.onChanged});
@@ -7,3 +7,29 @@ class FilterItem {
String caption;
Function onChanged;
}
class SwapFilterItem extends FilterItem {
SwapFilterItem({
required this.enabledProviders,
required this.allEnabled,
required super.value,
super.caption = "Swap",
required super.onChanged,
});
int Function() enabledProviders;
bool Function() allEnabled;
}
class SwapProviderFilterItem extends FilterItem {
SwapProviderFilterItem(
{required super.value,
required super.onChanged,
required this.providerDescription,
super.caption = ""});
@override
String get caption => providerDescription.title;
final ExchangeProviderDescription providerDescription;
}
@@ -1,43 +1,80 @@
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
import 'package:flutter/foundation.dart';
import "package:hash_wallet/view_model/dashboard/action_list_item.dart";
import "package:hash_wallet/view_model/dashboard/date_section_item.dart";
import "package:flutter/foundation.dart";
enum _DateBucket { recent, last7Days, last30Days, byMonth }
List<ActionListItem> formattedItemsList(List<ActionListItem> items) {
final formattedList = <ActionListItem>[];
DateTime? lastDate;
items.sort((a, b) => b.date.compareTo(a.date));
for (var i = 0; i < items.length; i++) {
final transaction = items[i];
final now = DateTime.now();
final todayTreshold = DateTime(now.year, now.month, now.day);
final last7daysThreshold = now.subtract(const Duration(days: 7));
final last30daysThreshold = now.subtract(const Duration(days: 30));
if (lastDate == null) {
lastDate = transaction.date;
formattedList.add(
DateSectionItem(
transaction.date,
key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'),
),
);
formattedList.add(transaction);
continue;
_DateBucket? lastBucket;
DateTime? lastMonthDate;
for (final transaction in items) {
final date = transaction.date;
final bucket = date.isAfter(todayTreshold)
? _DateBucket.recent
: date.isAfter(last7daysThreshold)
? _DateBucket.last7Days
: date.isAfter(last30daysThreshold)
? _DateBucket.last30Days
: _DateBucket.byMonth;
switch (bucket) {
case _DateBucket.recent:
if (lastBucket != _DateBucket.recent) {
formattedList.add(
TodayTransactionItem(
date,
key: const ValueKey("today_section_item_key"),
),
);
}
break;
case _DateBucket.last7Days:
if (lastBucket != _DateBucket.last7Days) {
formattedList.add(
Last7daysTransactionItem(
date,
key: const ValueKey("last_7_days_section_item_key"),
),
);
}
break;
case _DateBucket.last30Days:
if (lastBucket != _DateBucket.last30Days) {
formattedList.add(
Last30daysTransactionItem(
date,
key: const ValueKey("last_30_days_section_item_key"),
),
);
}
break;
case _DateBucket.byMonth:
final isNewMonth = lastMonthDate == null ||
lastMonthDate.year != date.year ||
lastMonthDate.month != date.month;
if (isNewMonth) {
lastMonthDate = date;
formattedList.add(
DateSectionItem(
date,
key: ValueKey("date_section_item_${date.year}_${date.month}_key"),
),
);
}
break;
}
final isCurrentDay = lastDate.year == transaction.date.year &&
lastDate.month == transaction.date.month &&
lastDate.day == transaction.date.day;
if (isCurrentDay) {
formattedList.add(transaction);
continue;
}
lastDate = transaction.date;
formattedList.add(
DateSectionItem(
transaction.date,
key: ValueKey('date_section_item_${transaction.date.microsecondsSinceEpoch}_key'),
),
);
lastBucket = bucket;
formattedList.add(transaction);
}
@@ -151,10 +151,10 @@ abstract class HomeSettingsViewModelBase with Store {
return false;
}
bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis(
contractAddress,
getChainNameBasedOnWalletType(_balanceViewModel.wallet.type),
);
final moralisChainName = evm!.getMoralisChainName(_balanceViewModel.wallet);
final isPotentialScamViaMoralis = moralisChainName != null &&
await _isPotentialScamTokenViaMoralis(contractAddress, moralisChainName);
bool isUnverifiedContract = await _isContractUnverified(
contractAddress,
@@ -333,6 +333,10 @@ abstract class HomeSettingsViewModelBase with Store {
return null;
}
Future<bool?> checkIfTokenIsVerifiedOnJupiter(String mintAddress) async {
return null; // Hash Bags: Solana removed
}
CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency;
void _updateFiatPrices(CryptoCurrency token) async {
@@ -0,0 +1,140 @@
import "dart:async";
import "package:hash_wallet/core/execution_state.dart";
import "package:hash_wallet/entities/solana_nft_asset_model.dart";
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:hash_wallet/view_model/contact_list/contact_list_view_model.dart";
import "package:cw_core/exceptions.dart";
import "package:cw_core/pending_transaction.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:mobx/mobx.dart";
part "nft_send_view_model.g.dart";
class NFTSendViewModel = NFTSendViewModelBase with _$NFTSendViewModel;
abstract class NFTSendViewModelBase with Store {
NFTSendViewModelBase(this._appStore, this._contactListViewModel);
final AppStore _appStore;
final ContactListViewModel _contactListViewModel;
@observable
ExecutionState state = InitialExecutionState();
@observable
PendingTransaction? pendingTransaction;
bool shouldRequireTOTP2FAFor(String destinationAddress) {
final settingsStore = _appStore.settingsStore;
final isContact = _contactListViewModel.contactsToShow
.any((contact) => contact.address == destinationAddress);
if (isContact) {
return settingsStore.shouldRequireTOTP2FAForSendsToContact;
}
final isInternalWallet = _contactListViewModel.walletContactsToShow
.any((contact) => contact.address == destinationAddress);
if (isInternalWallet) {
return settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
}
return settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
}
@action
Future<void> createTransaction(SolanaNFTAssetModel asset, String destinationAddress) async {
state = IsExecutingState();
try {
throw UnsupportedError('Solana NFTs are not supported');
state = ExecutedSuccessfullyState();
} catch (e) {
pendingTransaction = null;
state = FailureState(_errorMessage(e));
}
}
@action
Future<void> commitTransaction() async {
state = IsExecutingState();
try {
final wallet = _appStore.wallet!;
final transaction = pendingTransaction!;
await transaction.commit();
final signature = transaction.id;
pendingTransaction = null;
state = ExecutedSuccessfullyState();
if (signature.isNotEmpty) {
unawaited(_pollForTransaction(wallet, signature));
}
} catch (e) {
state = FailureState(_errorMessage(e));
}
}
/// Re-entrancy latch for the send button, held only while the tap handler is
/// inside [AuthService.authenticateAction].
///
/// Deliberately not [state]: the auth sheet is pushed closable, and closing it
/// pops the route without ever invoking onAuthenticationFinished, so a flag
/// that waits on the auth result would never be cleared. The caller clears
/// this in a finally instead. Once auth succeeds, createTransaction takes over
/// the guard by setting IsExecutingState synchronously.
bool _isStartingSend = false;
bool get isStartingSend => _isStartingSend;
void markSendStarted() => _isStartingSend = true;
void markSendSettled() => _isStartingSend = false;
@action
void reset() {
_isStartingSend = false;
pendingTransaction = null;
state = InitialExecutionState();
}
Future<void> _pollForTransaction(WalletBase wallet, String signature) async {
try {
} catch (e) {
printV("Failed to poll for NFT transfer: ${e.toString()}");
}
}
String _errorMessage(Object error) {
if (error is NotAnNFTException) {
return S.current.nft_send_not_an_nft;
}
if (error is NoAssociatedTokenAccountException) {
return S.current.solana_no_associated_token_account_exception;
}
if (error is SignSPLTokenTransactionRentException) {
return S.current.solana_sign_spl_token_transaction_rent_exception;
}
if (error is CreateAssociatedTokenAccountException) {
return "${S.current.solana_create_associated_token_account_exception} "
"${S.current.added_message_for_ata_error}";
}
printV("NFT send failed: ${error.toString()}");
return error.toString();
}
}
+171 -95
View File
@@ -1,12 +1,15 @@
import "dart:async";
import 'dart:convert';
import 'dart:developer';
import "package:hash_wallet/entities/saved_nfts.dart";
import 'package:hash_wallet/entities/solana_nft_asset_model.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:hash_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/utils/debounce.dart';
import "package:cw_core/utils/print_verbose.dart";
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:mobx/mobx.dart';
@@ -31,12 +34,27 @@ abstract class NFTViewModelBase with Store {
if (wallet != null) return wallet.chainId;
return null;
}, (_) => getNFTAssetByWallet());
}, (_) => _fetchDebounce.run(() => getNFTAssetByWallet()));
}
}
final AppStore appStore;
bool _nftErrorPopupShown = false;
final BottomSheetService bottomSheetService;
final Debounce _fetchDebounce = Debounce(const Duration(milliseconds: 500));
Future<void> _lastFetch = Future.value();
Completer<void>? _queuedFetch;
final SavedNFTs _savedNFTs = SavedNFTs();
int _currentRefreshId = 0;
static const _requestTimeout = Duration(seconds: 20);
Map<String, String> get _moralisHeaders => {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
};
@observable
bool isLoading;
@@ -44,137 +62,195 @@ abstract class NFTViewModelBase with Store {
@observable
bool isImportNFTLoading;
@observable
int unresolvedNFTCount = 0;
ObservableList<NFTAssetModel> nftAssetByWalletModels;
ObservableList<SolanaNFTAssetModel> solanaNftAssetModels;
// Hash Bags: Solana is not supported.
bool get isSolanaWallet => false;
String? get currentWalletName => appStore.wallet?.name;
@action
Future<void> getNFTAssetByWallet() async {
final wallet = appStore.wallet!;
void onWalletChanged() {
nftAssetByWalletModels.clear();
solanaNftAssetModels.clear();
unresolvedNFTCount = 0;
_nftErrorPopupShown = false;
_fetchDebounce.run(getNFTAssetByWallet);
}
if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) return;
@action
Future<void> getNFTAssetByWallet() {
final queued = _queuedFetch;
final walletAddress = wallet.walletInfo.address;
log('Fetching wallet NFTs for $walletAddress');
if (queued != null) {
return queued.future;
}
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: wallet.chainId);
// the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet
// the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response.
final done = Completer<void>();
_queuedFetch = done;
Uri uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/$walletAddress/nft',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"exclude_spam": "true",
"normalizeMetadata": "true",
},
);
final waitFor = _lastFetch;
_lastFetch = done.future;
unawaited(_fetchAfterPreviousRun(waitFor, done));
return done.future;
}
Future<void> _fetchAfterPreviousRun(Future<void> waitFor, Completer<void> done) async {
await waitFor;
if (identical(_queuedFetch, done)) {
_queuedFetch = null;
}
try {
if (isLoading) return;
final wallet = appStore.wallet!;
if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) {
return;
}
final walletAddress = wallet.walletInfo.address;
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: wallet.chainId);
isLoading = true;
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final refreshId = ++_currentRefreshId;
final decodedResponse = jsonDecode(response.body);
await _getEvmNFTAssets(wallet.name, walletAddress, chainName, refreshId);
final result =
WalletNFTsResponseModel.fromJson(decodedResponse as Map<String, dynamic>).result ?? [];
nftAssetByWalletModels.clear();
nftAssetByWalletModels.addAll(result);
_nftErrorPopupShown = false;
} catch (e) {
log(e.toString());
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.moralis_nft_error,
),
);
printV("Fetching wallet NFTs failed: ${e.toString()}");
if (!_nftErrorPopupShown) {
_nftErrorPopupShown = true;
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.moralis_nft_error,
),
);
}
} finally {
isLoading = false;
done.complete();
}
}
Future<SolanaNFTAssetModel> getSolanaNFTDetails(String address, String chainName) async {
final uri = Uri.https(
'solana-gateway.moralis.io',
'/nft/$chainName/$address/metadata',
);
Future<void> _getEvmNFTAssets(
String walletName,
String walletAddress,
String chainName,
int refreshId,
) async {
final saved = await _savedNFTs.evmNFTs(walletName, chainName);
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
var result = const <NFTAssetModel>[];
Object? indexerError;
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
try {
final response = await ProxyWrapper()
.get(
clearnetUri: Uri.https(
"deep-index.moralis.io",
"/api/v2.2/$walletAddress/nft",
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"exclude_spam": "true",
"normalizeMetadata": "true",
},
),
headers: _moralisHeaders,
)
.timeout(_requestTimeout);
return SolanaNFTAssetModel.fromJson(decodedResponse);
result = WalletNFTsResponseModel.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
).result ??
[];
} catch (e) {
indexerError = e;
}
if (appStore.wallet?.walletInfo.address != walletAddress || refreshId != _currentRefreshId) {
return;
}
final listed = result.map(SavedNFTs.evmIdentifier).toSet();
nftAssetByWalletModels.clear();
nftAssetByWalletModels.addAll(result);
nftAssetByWalletModels
.addAll(saved.where((asset) => !listed.contains(SavedNFTs.evmIdentifier(asset))));
if (indexerError != null) {
throw indexerError;
}
}
@action
Future<void> onNFTSent(String walletName, String mint) async {
await _savedNFTs.removeSolana(walletName, mint);
if (appStore.wallet?.name == walletName) {
solanaNftAssetModels.removeWhere((asset) => asset.mint == mint);
}
}
Future<void> _importEvmNFT(
String walletName,
String tokenAddress,
String? tokenId,
String chainName,
) async {
final response = await ProxyWrapper()
.get(
clearnetUri: Uri.https(
"deep-index.moralis.io",
"/api/v2.2/nft/$tokenAddress/$tokenId",
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"normalizeMetadata": "true",
},
),
headers: _moralisHeaders,
)
.timeout(_requestTimeout);
final asset = NFTAssetModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
nftAssetByWalletModels.add(asset);
await _savedNFTs.addEvm(walletName, chainName, asset);
}
@action
Future<void> importNFT(String tokenAddress, String? tokenId) async {
final walletType = appStore.wallet!.type;
final wallet = appStore.wallet!;
int? chainId;
if (isEVMCompatibleChain(walletType)) {
chainId = evm!.getSelectedChainId(appStore.wallet!);
if (isEVMCompatibleChain(wallet.type)) {
chainId = evm!.getSelectedChainId(wallet);
}
final chainName = getChainNameBasedOnWalletType(walletType, chainId: chainId);
// the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: chainId);
try {
isImportNFTLoading = true;
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/nft/$tokenAddress/$tokenId',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"normalizeMetadata": "true",
},
);
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
final nftAsset = NFTAssetModel.fromJson(decodedResponse);
nftAssetByWalletModels.add(nftAsset);
await _importEvmNFT(wallet.name, tokenAddress, tokenId, chainName);
} catch (e) {
printV("Importing NFT $tokenAddress failed: ${e.toString()}");
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: e.toString(),
message: S.current.nft_import_failed,
),
);
} finally {
@@ -22,7 +22,9 @@ abstract class ReceiveOptionViewModelBase with Store {
@observable
ReceivePageOption selectedReceiveOption;
List<ReceivePageOption> get options => _wallet.walletAddresses.receivePageOptions;
@computed
List<ReceivePageOption> get options =>
_wallet.walletAddresses.receivePageOptions.where(_wallet.receiveOptionAvailable).toList();
String get walletTypeString => walletTypeToString(_wallet.type);
@@ -2,17 +2,13 @@ import 'package:hash_wallet/entities/balance_display_mode.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/nano/nano.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount_raw.dart';
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/keyable.dart';
@@ -44,22 +40,26 @@ class TransactionListItem extends ActionListItem with Keyable {
String get formattedCryptoAmount {
if (displayMode == BalanceDisplayMode.hiddenBalance) return '---';
if (balanceViewModel.wallet.type == WalletType.bitcoin) {
final isLightning = (transaction.additionalInfo["isLightning"] as bool?) ?? false;
final crypto = isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc;
final amount = _appStore.amountParsingProxy
.getDisplayCryptoString(transaction.amount, crypto)
.withMaxDecimals(8)
return _appStore.amountParsingProxy
.asDisplayStringWithSymbol(transaction.amount)
.withLocalSeperator(_appStore.settingsStore.languageCode);
return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}';
}
return transaction.amountFormatted();
return transaction.amount.toStringWithSymbol(fractionalDigits: 8);
}
String get formattedTitle {
if (transaction.additionalInfo['autoShield'] == true) {
return "Autoshield";
if (transaction.additionalInfo['isIronwoodMigration'] == true) {
return 'Migration';
}
if (transaction.additionalInfo['isAutoShield'] == true) {
if (transaction.isPending) {
final status = formattedStatus;
final baseString = S.current.shielding;
return status.isNotEmpty ? "$baseString $status" : "$baseString...";
}
return S.current.shielding;
}
if (transaction.isPending) {
final status = formattedStatus;
@@ -168,42 +168,25 @@ class TransactionListItem extends ActionListItem with Keyable {
switch (balanceViewModel.wallet.type) {
case WalletType.monero:
amount = calculateFiatAmountRaw(
cryptoAmount: monero!.formatterMoneroAmountToDouble(amount: transaction.amount),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.wownero:
amount = calculateFiatAmountRaw(
cryptoAmount: wownero!.formatterWowneroAmountToDouble(amount: transaction.amount),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.bitcoinCash:
case WalletType.dogecoin:
case WalletType.nano:
amount = calculateFiatAmountRaw(
cryptoAmount: bitcoin!.formatterBitcoinAmountToDouble(amount: transaction.amount),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
final asset = evm!.assetOfTransaction(balanceViewModel.wallet, transaction);
final asset = assetOfTransaction;
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: evm!.formatterEVMAmountToDouble(transaction: transaction),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.nano:
amount = calculateFiatAmountRaw(
cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString(
nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
@@ -1,6 +0,0 @@
class WalletBalance {
WalletBalance({required this.unlockedBalance, required this.totalBalance});
String unlockedBalance;
String totalBalance;
}
+15 -19
View File
@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:hash_wallet/core/key_service.dart';
import 'package:hash_wallet/di.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
@@ -56,26 +55,24 @@ abstract class DevMoneroBackgroundSyncBase with Store {
bool isBackgroundSyncing = false;
Future<void> _setValues() async {
final w = (wallet as MoneroWallet);
localBlockHeight = (await monero!.getCurrentHeight()).toString();
nodeBlockHeight = (await w.getNodeHeight()).toString();
final keys = w.keys;
primaryAddress = keys.primaryAddress;
publicViewKey = keys.publicViewKey;
privateViewKey = keys.privateViewKey;
publicSpendKey = keys.publicSpendKey;
privateSpendKey = keys.privateSpendKey;
passphrase = keys.passphrase;
seed = w.seed;
seedLegacy = w.seedLegacy("English");
nodeBlockHeight = (await monero!.getNodeHeight(wallet)).toString();
final keys = monero!.getKeys(wallet);
primaryAddress = keys["primaryAddress"];
publicViewKey = keys["publicViewKey"];
privateViewKey = keys["privateViewKey"];
publicSpendKey = keys["publicSpendKey"];
privateSpendKey = keys["privateSpendKey"];
passphrase = keys["passphrase"];
seed = wallet.seed;
seedLegacy = monero!.getLegacySeed(wallet, "English");
tick = refreshTimer?.tick ?? -1;
isBackgroundSyncing = w.isBackgroundSyncRunning;
isBackgroundSyncing = monero!.isBackgroundSyncRunning(wallet);
}
@action
Future<void> manualRescan() async {
final w = (wallet as MoneroWallet);
await wallet.rescan(height: await w.getNodeHeight() - 10000);
await wallet.rescan(height: await monero!.getNodeHeight(wallet) - 10000);
}
@action
@@ -93,14 +90,13 @@ abstract class DevMoneroBackgroundSyncBase with Store {
@action
void startBackgroundSync() {
final w = (wallet as MoneroWallet);
w.startBackgroundSync();
unawaited(monero!.startBackgroundSync(wallet));
}
@action
Future<void> stopBackgroundSync() async {
final w = (wallet as MoneroWallet);
final keyService = getIt.get<KeyService>();
await w.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name));
final password = await keyService.getWalletPassword(walletName: wallet.name);
await monero!.stopBackgroundSync(wallet, password);
}
}
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:hash_wallet/core/amount_parsing_proxy.dart';
import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
@@ -13,6 +14,7 @@ import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:hash_wallet/store/dashboard/trades_store.dart';
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
import 'package:hash_wallet/utils/qr_util.dart';
import 'package:hash_wallet/utils/token_utilities.dart';
import 'package:hash_wallet/view_model/send/fees_view_model.dart';
@@ -37,10 +39,8 @@ abstract class ExchangeTradeViewModelBase with Store {
required this.feesViewModel,
required this.fiatConversionStore,
}) : trade = tradesStore.trade!,
isSendable = _checkIfCanSend(tradesStore, wallet),
isSwapsXYZCanSendFromExternal =
_checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet),
items = ObservableList<ExchangeTradeItem>() {
_checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet) {
setUpOutput();
// Trades from providers whose implementations were removed keep a null
// _provider; status polling is simply skipped for them.
@@ -53,7 +53,6 @@ abstract class ExchangeTradeViewModelBase with Store {
break;
}
_updateItems();
if (_provider != null) {
_updateTrade();
@@ -73,11 +72,10 @@ abstract class ExchangeTradeViewModelBase with Store {
@observable
Trade trade;
@observable
bool isSendable;
bool isSwapsXYZCanSendFromExternal;
bool get isSendable => checkIfCanSend(trade, wallet) == null;
/// Returns true if the current provider should hide the external send button
bool get shouldHideExternalSendButton {
if (_provider == null) return false;
@@ -101,8 +99,6 @@ abstract class ExchangeTradeViewModelBase with Store {
? ""
: "${sendViewModel.pendingTransactionFeeFiatAmount} ${sendViewModel.fiat.title}";
@observable
ObservableList<ExchangeTradeItem> items;
ExchangeProvider? _provider;
@@ -161,7 +157,13 @@ abstract class ExchangeTradeViewModelBase with Store {
@action
Future<void> confirmSending() async {
if (!isSendable) return;
final canSendError = checkIfCanSend(trade, wallet);
if (canSendError != null) {
_logCanSendError(trade, wallet, canSendError);
sendViewModel.state = FailureState(canSendError);
return;
}
final selected = trade.from;
if (selected == null) {
@@ -183,150 +185,60 @@ abstract class ExchangeTradeViewModelBase with Store {
await trade.save();
tradesStore.setTrade(trade);
_updateItems();
} catch (e) {
printV(e.toString());
}
}
void _updateItems() {
String? checkIfCanSend(Trade? trade, WalletBase wallet) {
if (trade == null) return 'Trade is null';
final tradeFrom = trade.from;
final tradeTo = trade.to;
if (tradeFrom == null) return 'Trade from currency is null';
final tagFrom = tradeFrom?.tag != null ? "${tradeFrom!.tag} " : "";
final tagTo = tradeTo?.tag != null ? "${tradeTo!.tag} " : "";
bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b);
items.clear();
if (trade.provider != ExchangeProviderDescription.thorChain)
items.add(
ExchangeTradeItem(
title: "${trade.provider.title} ${S.current.id}",
data: "${trade.id}",
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
);
if (tradeFrom != null && tradeTo != null) {
items.addAll([
ExchangeTradeItem(
title: S.current.amount,
data:
"${_amountParsingProxy.getDisplayCryptoAmount(trade.amount, tradeFrom)} ${_amountParsingProxy.getCryptoSymbol(tradeFrom)}",
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true,
),
ExchangeTradeItem(
title: "${S.current.you_will_receive_estimated_amount}:",
data:
"${_amountParsingProxy.getDisplayCryptoAmount(trade.receiveAmount ?? "0", tradeTo)} ${_amountParsingProxy.getCryptoSymbol(tradeTo)}",
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
ExchangeTradeItem(
title: "${S.current.send_to_this_address("$tradeFrom", tagFrom)}:",
data: trade.inputAddress ?? '',
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true,
),
]);
items.add(
isSwapsXYZCanSendFromExternal
? ExchangeTradeItem(
title: S.current.send_to_this_address('${tradeFrom}', tagFrom) + ':',
data: trade.inputAddress ?? '',
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true)
: ExchangeTradeItem(
title: 'Smart contract call (no address required)',
data: 'Wallet will execute a contract call. On-chain transaction',
isCopied: false,
isReceiveDetail: false,
isExternalSendDetail: true),
);
bool _isTokenBelongingToWallet(CryptoCurrency cur) {
final chainTag = cur.tag ?? cur.title;
return wallet.currency == cur &&
(tradeFrom.tag?.toUpperCase() == chainTag.toUpperCase() ||
tradeFrom.title.toUpperCase() == chainTag.toUpperCase());
}
final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty;
final canSend = _sameCurrency(tradeFrom, wallet.currency) ||
(_sameCurrency(tradeFrom, CryptoCurrency.btcln) && wallet.currency == CryptoCurrency.btc) ||
trade.provider == ExchangeProviderDescription.xmrto ||
_isTokenBelongingToWallet(CryptoCurrency.eth) ||
_isTokenBelongingToWallet(CryptoCurrency.maticpoly) ||
_isTokenBelongingToWallet(CryptoCurrency.baseEth) ||
_isTokenBelongingToWallet(CryptoCurrency.arbEth) ||
_isTokenBelongingToWallet(CryptoCurrency.trx) ||
_isTokenBelongingToWallet(CryptoCurrency.sol) ||
_isTokenBelongingToWallet(CryptoCurrency.bnb);
if (isExtraIdExist) {
final title = tradeFrom == CryptoCurrency.xrp
? S.current.destination_tag
: [CryptoCurrency.xlm, CryptoCurrency.ton].contains(tradeFrom)
? S.current.memo
: S.current.extra_id;
items.add(
ExchangeTradeItem(
title: title,
data: trade.extraId ?? "",
isCopied: true,
isReceiveDetail: !isExtraIdExist,
isExternalSendDetail: isExtraIdExist,
),
);
if (!canSend) {
return 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.';
}
items.add(
ExchangeTradeItem(
title: "${S.current.arrive_in_this_address("${tradeTo}", tagTo)}:",
data: trade.payoutAddress ?? "",
isCopied: true,
isReceiveDetail: true,
isExternalSendDetail: false,
),
);
return null;
}
static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) {
final trade = tradesStore.trade!;
final tradeFrom = trade.from;
bool _sameCurrency(CryptoCurrency? a, CryptoCurrency? b) =>
a != null && b != null && a.titleAndTagEqual(b);
bool _isEthToken() =>
wallet.currency == CryptoCurrency.eth && tradeFrom?.tag == CryptoCurrency.eth.title;
bool _isPolygonToken() =>
wallet.currency == CryptoCurrency.maticpoly &&
tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
bool _isBaseToken() =>
wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag;
bool _isArbitrumToken() =>
wallet.currency == CryptoCurrency.arbEth &&
(tradeFrom?.tag == CryptoCurrency.arbEth.tag ||
tradeFrom?.title ==
CryptoCurrency.arbEth
.tag); // This is to handle the CryptoCurrency.arb that doesn't have a tag but fully belongs to the Arbitrum chain
bool _isTronToken() =>
wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title;
bool _isSplToken() =>
wallet.currency == CryptoCurrency.sol && tradeFrom?.tag == CryptoCurrency.sol.title;
bool _isBscToken() =>
wallet.currency == CryptoCurrency.bnb && tradeFrom?.tag == CryptoCurrency.bnb.tag;
return _sameCurrency(tradeFrom, wallet.currency) ||
(_sameCurrency(tradeFrom, CryptoCurrency.btcln) && wallet.currency == CryptoCurrency.btc) ||
tradesStore.trade!.provider == ExchangeProviderDescription.xmrto ||
_isEthToken() ||
_isPolygonToken() ||
_isSplToken() ||
_isTronToken() ||
_isBaseToken() ||
_isArbitrumToken() ||
_isBscToken();
void _logCanSendError(Trade? trade, WalletBase wallet, String error) {
ExchangeProviderLogger.logError(
provider: trade?.provider,
function: '_checkIfCanSend',
error: error,
requestData: {
'tradeId': trade?.id,
'tradeFrom': trade?.from?.title,
'tradeFromTag': trade?.from?.tag,
'tradeTo': trade?.to?.title,
'tradeToTag': trade?.to?.tag,
'walletName': wallet.name,
'walletCurrency': wallet.currency.title,
'walletCurrencyTag': wallet.currency.tag,
},
);
}
static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) {
@@ -349,8 +261,13 @@ abstract class ExchangeTradeViewModelBase with Store {
return null;
}
// Using trade currency here so the external-send QR encodes the correct scheme
final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency) ?? wallet.type;
// Using the trade's `from` currency so the external-send QR encodes the correct scheme.
final uriWalletType = cryptoCurrencyOrTokenToWalletType(fromCurrency);
// for other currencies that we don't have wallets for
if (uriWalletType == null) {
return ExternalAddressURI(address: inputAddress, amount: amount);
}
printV(uriWalletType);
@@ -362,16 +279,11 @@ abstract class ExchangeTradeViewModelBase with Store {
case WalletType.dogecoin:
return DogeURI(address: inputAddress, amount: amount);
case WalletType.ethereum:
return _createERC681URI(fromCurrency, inputAddress, amount);
// TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling)
case WalletType.polygon:
return PolygonURI(amount: amount, address: inputAddress);
case WalletType.base:
return BaseURI(amount: amount, address: inputAddress);
case WalletType.arbitrum:
return ArbitrumURI(amount: amount, address: inputAddress);
case WalletType.bsc:
return BSCURI(amount: amount, address: inputAddress);
return _createERC681URI(fromCurrency, inputAddress, amount);
case WalletType.monero:
return MoneroURI(address: inputAddress, amount: amount);
case WalletType.wownero:
@@ -408,6 +320,7 @@ abstract class ExchangeTradeViewModelBase with Store {
address: address,
amount: amount,
contractAddress: erc20Token.contractAddress,
tokenDecimals: erc20Token.decimal,
);
}
return null;
+209 -139
View File
@@ -28,16 +28,16 @@ import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:hash_wallet/exchange/trade.dart';
import 'package:hash_wallet/exchange/trade_request.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
import 'package:hash_wallet/utils/show_pop_up.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/amount/money_double.dart';
import "package:cw_core/wallet_info.dart";
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:hash_wallet/store/dashboard/trades_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/store/templates/exchange_template_store.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/utils/token_utilities.dart';
@@ -46,6 +46,7 @@ import 'package:hash_wallet/view_model/send/fees_view_model.dart';
import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currencies_with_memo.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/sync_status.dart';
@@ -83,7 +84,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
ExchangeViewModelBase(
this._appStore,
this._exchangeTemplateStore,
this.tradesStore,
this.sharedPreferences,
this.contactListViewModel,
@@ -93,8 +93,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
) : isSendAllEnabled = false,
isFixedRateMode = false,
isReceiveAmountEntered = false,
_depositAmount = '',
_receiveAmount = '',
_depositAmount = null,
_receiveAmount = null,
receiveAddress = '',
depositAddress = '',
isDepositAddressEnabled = false,
@@ -192,8 +192,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
});
isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
receiveAddress = '';
depositAddress =
useSameWalletAddress(depositCurrency) ? wallet.walletAddresses.addressForExchange : '';
@@ -261,12 +261,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
final balance = wallet.balance[depositCurrency];
if (balance != null) {
depositAvailableAmount = _appStore.amountParsingProxy
.getDisplayCryptoStringFromBigInt(balance.fullAvailableBalance, depositCurrency);
depositAvailableAmount = _appStore.amountParsingProxy.asDisplayString(balance.available);
return false;
}
return true;
});
}).timeout(Duration(seconds: 20));
} else if (isEVMCompatibleChain(wallet.type)) {
final currency = depositCurrency;
final balanceCurrency = wallet.balance.keys.firstWhereOrNull(
@@ -276,14 +275,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
);
final balanceForCurrency = balanceCurrency != null ? wallet.balance[balanceCurrency] : null;
if (depositCurrency == currency && balanceForCurrency != null) {
depositAvailableAmount = _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(
balanceForCurrency.fullAvailableBalance, balanceCurrency!);
depositAvailableAmount =
_appStore.amountParsingProxy.asDisplayStringWithSymbol(balanceForCurrency.available);
}
} else {
final currency = depositCurrency;
final amount = _appStore.amountParsingProxy.getDisplayCryptoString(
(await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any)),
depositCurrency);
final sendingBalance = Money.fromInt(
await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any), currency);
final amount = _appStore.amountParsingProxy.asDisplayStringWithSymbol(sendingBalance);
if (depositCurrency == currency) {
depositAvailableAmount = amount;
}
@@ -291,17 +290,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
void showFiatCurrencyPicker(BuildContext context) {
showPopUp<void>(
FiatCurrencyPickerSheet.show(
context: context,
builder: (_) => CurrencyPicker(
key: ValueKey('send_page_currency_picker_dialog_button_key'),
selectedAtIndex: FiatCurrency.all.indexOf(fiat),
items: FiatCurrency.all,
hintText: S.of(context).search_currency,
onItemSelected: (Currency cur) async {
_settingsStore.fiatCurrency = (cur as FiatCurrency);
},
),
selected: fiat,
onSelected: (cur) => _settingsStore.fiatCurrency = cur,
);
}
@@ -321,10 +313,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
].contains(wallet.type);
bool get hideAddressAfterExchange =>
[WalletType.monero, WalletType.wownero].contains(wallet.type);
[WalletType.monero, WalletType.wownero].contains(wallet.type) || isElectrumWallet;
bool _useTorOnly;
final ExchangeTemplateStore _exchangeTemplateStore;
final TradesStore tradesStore;
final SharedPreferences sharedPreferences;
@@ -378,27 +369,29 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
ExchangeTradeState tradeState;
@observable
String _depositAmount;
Money? _depositAmount;
@computed
String get depositAmount => _depositAmount == "..."
? _depositAmount
: amountParsingProxy.getDisplayCryptoAmount(_depositAmount, depositCurrency);
bool get hasDepositAmount => _depositAmount != null;
@computed
String get depositAmountCanonical => _depositAmount;
String get depositAmount =>
_depositAmount == null ? "" : amountParsingProxy.asDisplayString(_depositAmount!);
@computed
String get depositAmountCanonical => _depositAmount == null ? "0.0" : _depositAmount.toString();
@observable
String _receiveAmount;
Money? _receiveAmount;
@computed
String get receiveAmount => _receiveAmount == "..."
? _receiveAmount
: amountParsingProxy.getDisplayCryptoAmount(_receiveAmount, receiveCurrency);
String get receiveAmount =>
_receiveAmount == null ? "" : amountParsingProxy.asDisplayString(_receiveAmount!);
@action
// only set canonical formated amounts here;
void setCanonicalReceiveAmount(String value) => _receiveAmount = value;
void setCanonicalReceiveAmount(String value) =>
_receiveAmount = Money.tryParse(value, receiveCurrency);
@observable
String depositAddress;
@@ -406,6 +399,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@observable
String receiveAddress;
@observable
String receiveAddressExtraId = '';
@observable
String? receiveAddressDisplayName;
@@ -430,14 +426,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@observable
bool tradeStarted = false;
@observable
bool noProviderForPair = false;
@observable
bool isSendFromExternal = false;
@computed
SyncStatus get status => wallet.syncStatus;
@computed
ObservableList<ExchangeTemplate> get templates => _exchangeTemplateStore.templates;
@computed
List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts
@@ -451,14 +448,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@computed
Future<List<WalletInfo>> get receiveWallets async {
WalletType? type;
type = cryptoCurrencyOrTokenToWalletType(receiveCurrency);
if (type == null) {
type =
cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? ""));
}
try {
WalletType? type;
type = cryptoCurrencyOrTokenToWalletType(receiveCurrency);
if (type == null) {
type =
cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? ""));
}
return await WalletInfo.selectList("type = ?", [type!.index]);
return await WalletInfo.selectList("type = ?", [type!.index]);
} catch (e) {
return [];
}
}
Future<List<WalletInfoAddressInfo>> addressesForAccountsWallet(WalletInfo wallet) async {
@@ -517,11 +518,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
} else {
balanceCurrency = depositCurrency;
}
final bal =
balanceCurrency != null ? wallet.balance[balanceCurrency]?.fullAvailableBalance : null;
final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.available : null;
if (bal == null) return null;
return amountParsingProxy.getDisplayCryptoStringFromBigInt(
bal, balanceCurrency ?? depositCurrency);
return amountParsingProxy.asDisplayString(bal);
}
//* Still open to further optimize these checks
@@ -596,10 +595,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
double bestRate = 0.0;
@computed
bool get useSatoshiDeposit => _appStore.amountParsingProxy.useSatoshi(depositCurrency);
bool get useDepositBaseUnit => _appStore.amountParsingProxy.useSatoshi(depositCurrency);
@computed
bool get useSatoshisReceive => _appStore.amountParsingProxy.useSatoshi(receiveCurrency);
bool get useReceiveBaseUnit => _appStore.amountParsingProxy.useSatoshi(receiveCurrency);
@computed
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
@@ -648,12 +647,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
String get receiveAmountFiatFormatted {
var amount = '0.00';
try {
if (_receiveAmount.isNotEmpty) {
if (_receiveAmount != null) {
if (fiatConversionStore.prices[receiveCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[receiveCurrency]!,
cryptoAmount: _receiveAmount,
cryptoAmount: _receiveAmount.toString(),
);
}
} catch (_) {
@@ -666,12 +665,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
String get depositAmountFiatFormatted {
var amount = '0.00';
try {
if (_depositAmount.isNotEmpty) {
if (_depositAmount != null) {
if (fiatConversionStore.prices[depositCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[depositCurrency]!,
cryptoAmount: _depositAmount,
cryptoAmount: _depositAmount.toString(),
);
}
} catch (_) {
@@ -684,12 +683,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
String get receiveAmountFiat {
var amount = '';
try {
if (_receiveAmount.isNotEmpty) {
if (_receiveAmount != null) {
if (fiatConversionStore.prices[receiveCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[receiveCurrency]!,
cryptoAmount: _receiveAmount,
cryptoAmount: _receiveAmount.toString(),
raw: true);
}
} catch (_) {
@@ -702,13 +701,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
String get depositAmountFiat {
var amount = '';
try {
if (_depositAmount.isNotEmpty) {
if (_depositAmount != null) {
if (fiatConversionStore.prices[depositCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[depositCurrency]!,
cryptoAmount: _depositAmount,
raw: true);
price: fiatConversionStore.prices[depositCurrency]!,
cryptoAmount: _depositAmount.toString(),
raw: true,
);
}
} catch (_) {
log('Error calculating deposit amount fiat formatted: $_');
@@ -780,6 +780,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
currency == receiveCurrency)) {
receiveAddress = "";
}
if (currency != receiveCurrency) {
receiveAddressExtraId = "";
}
receiveCurrency = currency;
isFixedRateMode = false;
@@ -792,32 +795,37 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
@action
Future<void> changeReceiveAmount({required String amount}) async {
amount = amountParsingProxy.getCanonicalCryptoAmount(amount, receiveCurrency);
_receiveAmount = amount;
Future<void> changeReceiveAmount({required String amount, bool isCanonical = false}) async {
if (amount.isEmpty) {
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
return;
}
final _enteredAmount = double.tryParse(_receiveAmount.replaceAll(',', '.')) ?? 0;
_receiveAmount = isCanonical
? Money.tryParse(amount.sanitized(), receiveCurrency)
: _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), receiveCurrency);
if (_receiveAmount == null) {
_depositAmount = null;
return;
}
final _enteredAmount = double.tryParse(_receiveAmount.toString()) ?? 0;
if (bestRate == 0) {
_depositAmount = "...";
_depositAmount = null;
await calculateBestRate();
}
final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate);
_depositAmount = amount_.toString().withMaxDecimals(depositCurrency.decimals);
_depositAmount = amount_.tryToMoney(depositCurrency);
}
@action
void setReceiveAmountFromFiat({required String fiatAmount}) {
final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0;
final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0;
final price = fiatConversionStore.prices[receiveCurrency];
if (price == null || price == 0.0) return;
@@ -830,7 +838,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
void setDepositAmountFromFiat({required String fiatAmount}) {
final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0;
final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0;
final price = fiatConversionStore.prices[depositCurrency];
if (price == null || price == 0.0) return;
@@ -843,14 +851,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
Future<void> changeDepositAmount({required String amount, bool isCanonical = false}) async {
_depositAmount = isCanonical
? amount.replaceAll(',', '.')
: _appStore.amountParsingProxy
.getCanonicalCryptoAmount(amount.replaceAll(',', '.'), depositCurrency);
if (amount.isEmpty) {
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
return;
}
_depositAmount = isCanonical
? Money.tryParse(amount.sanitized(), depositCurrency)
: _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), depositCurrency);
if (_depositAmount == null) {
_receiveAmount = null;
return;
}
@@ -858,21 +870,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
/// as it should remain exactly what the user set
if (isFixedRateMode) return;
final _enteredAmount = double.tryParse(_depositAmount) ?? 0;
final _enteredAmount = double.tryParse(_depositAmount.toString()) ?? 0;
/// in case the best rate was not calculated yet
if (bestRate == 0) {
_receiveAmount = "...";
_receiveAmount = null;
await calculateBestRate();
}
final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate);
_receiveAmount = amount_.toString().withMaxDecimals(receiveCurrency.decimals);
_receiveAmount = amount_.tryToMoney(receiveCurrency);
}
bool checkIfInputMeetsMinOrMaxCondition(String input) {
final _enteredAmount = double.tryParse(input.replaceAll(',', '.')) ?? 0;
final _enteredAmount = double.tryParse(input.sanitized()) ?? 0;
double minLimit = limits.min ?? 0;
double? maxLimit = limits.max;
@@ -889,8 +901,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return;
}
final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final amount =
double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
forcedProviderRate = await forcedProvider!.fetchRate(
from: depositCurrency,
@@ -905,17 +918,22 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
(provider.description == ExchangeProviderDescription.swapsXyz ||
provider.description == ExchangeProviderDescription.nearIntents);
bool _excludeProviderForReceiveExtraId(ExchangeProvider provider) =>
memoLabelTypeFor(receiveCurrency) != null && !provider.supportsMemoOrDestinationTag;
Future<void> calculateBestRate() async {
if (depositCurrency == receiveCurrency) {
bestRate = 0.0;
bestRateProvider = null;
return;
}
final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final amount =
double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
if (_excludeProviderForSwapAll(provider)) return false;
if (_excludeProviderForReceiveExtraId(provider)) return false;
final limits = _providerLimits[provider];
@@ -972,6 +990,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bestRate = _sortedAvailableProviders.keys.first;
bestRateProvider = _sortedAvailableProviders.values.first;
}
noProviderForPair = _sortedAvailableProviders.isEmpty;
}
@action
@@ -993,6 +1012,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
try {
final futures = selectedProviders
.where((provider) => providerList.contains(provider))
.where((provider) => !_excludeProviderForReceiveExtraId(provider))
.map((provider) async {
final limits = await provider
.fetchLimits(
@@ -1041,19 +1061,73 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
Future<void> createTrade() async {
// Ignore repeated taps while a trade is already being created
if (tradeState is TradeIsCreating) return;
// Set synchronously, before the first await, so the swap button shows its
// loading state and further taps are ignored right away
tradeState = TradeIsCreating();
try {
await _createTrade();
} finally {
// Never leave the swap button stuck in its loading state
// (e.g. if an unexpected exception is thrown)
if (tradeState is TradeIsCreating) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange);
}
}
}
Future<void> _createTrade() async {
final depositAmountValue = _depositAmount ?? Money.zero(depositCurrency);
final receiveAmountValue = _receiveAmount ?? Money.zero(receiveCurrency);
if (depositAmountValue.isZero ||
depositAmountValue.isNegative ||
receiveAmountValue.isZero ||
receiveAmountValue.isNegative) {
ExchangeProviderLogger.logError(
provider: forcedProvider?.description ?? bestRateProvider?.description,
function: 'createTrade',
error: 'Invalid swap amount: deposit=$_depositAmount receive=$_receiveAmount',
requestData: {
'from': depositCurrency.title,
'to': receiveCurrency.title,
'fromAmount': _depositAmount,
'toAmount': _receiveAmount,
'isFixedRateMode': isFixedRateMode,
'forcedProvider': forcedProvider?.title,
'bestRateProvider': bestRateProvider?.title,
},
);
final invalidAmountError = depositAmountValue.isZero || depositAmountValue.isNegative
? '$depositAmountValue is not a valid amount for depositAmount'
: '$receiveAmountValue is not a valid amount for receiveAmount';
tradeState =
TradeIsCreatedFailure(title: S.current.trade_not_created, error: invalidAmountError);
return;
}
if (isSendAllEnabled) {
await calculateDepositAllAmount();
final amount = double.tryParse(_depositAmount);
final amount = double.tryParse(_depositAmount.toString());
if (limits.min != null && amount != null && amount < limits.min!) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.amount_is_below_minimum_limit(limits.min!.toString()));
title: S.current.trade_not_created,
error: S.current.amount_is_below_minimum_limit(limits.min!.toString()),
);
return;
}
}
if (depositCurrency == CryptoCurrency.btcln &&
wallet.type == WalletType.bitcoin &&
depositAddress == wallet.walletAddresses.addressForExchange) {
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
if (invoice != null) {
@@ -1062,6 +1136,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
if (receiveCurrency == CryptoCurrency.btcln &&
wallet.type == WalletType.bitcoin &&
receiveAddress == wallet.walletAddresses.addressForExchange) {
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
if (invoice != null) {
@@ -1069,6 +1144,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
if (forcedProvider != null && _excludeProviderForReceiveExtraId(forcedProvider!)) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange,
);
return;
}
Map<double, ExchangeProvider> providers;
if (forcedProvider != null) {
providers = {forcedProviderRate: forcedProvider!};
@@ -1158,6 +1241,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
continue;
}
if (_excludeProviderForReceiveExtraId(provider)) {
continue;
}
// Skip Swaps.xyz when sending from external
if (isSendFromExternal && provider.description == ExchangeProviderDescription.swapsXyz) {
printV('Skipping Swaps.xyz for external send');
@@ -1169,15 +1256,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bestRate = providerRate;
bestRateProvider = provider;
await changeDepositAmount(amount: _depositAmount, isCanonical: true);
await changeDepositAmount(amount: _depositAmount.toString(), isCanonical: true);
final request = TradeRequest(
fromCurrency: depositCurrency,
toCurrency: receiveCurrency,
fromAmount: _depositAmount.replaceAll(',', '.'),
toAmount: _receiveAmount.replaceAll(',', '.'),
fromAmount: _depositAmount.toString(),
toAmount: _receiveAmount.toString(),
refundAddress: depositAddress,
toAddress: receiveAddress,
toAddressExtraId: receiveAddressExtraId.trim(),
isFixedRate: isFixedRateMode,
);
@@ -1186,8 +1274,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
await wallet.walletAddresses.saveAddressesInBox();
}
var amount = isFixedRateMode ? _receiveAmount : _depositAmount;
amount = amount.replaceAll(',', '.');
var amount = isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString();
if (limitsState is LimitsLoadedSuccessfully) {
if (double.tryParse(amount) == null) {
@@ -1210,6 +1297,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
trade.walletId = wallet.id;
trade.chainId = wallet.chainId;
trade.fromWalletAddress = wallet.walletAddresses.address;
if (trade.from == null) {
trade.from = depositCurrency;
}
if (trade.to == null) {
trade.to = receiveCurrency;
}
final canCreateTrade = await isCanCreateTrade(trade);
if (!canCreateTrade.result) {
@@ -1264,7 +1357,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
///
/// this is because the limitation of the SplayTreeMap that
/// you can't modify it while iterating through it
Future.delayed(Duration(milliseconds: 200), createTrade);
await Future<void>.delayed(Duration(milliseconds: 200));
await _createTrade();
}
}
@@ -1272,12 +1366,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
void reset() {
_initialPairBasedOnWallet();
isReceiveAmountEntered = false;
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
depositAddress =
depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
receiveAddress =
receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
receiveAddressExtraId = '';
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
isFixedRateMode = false;
_onPairChange();
@@ -1308,8 +1403,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final amount = depositCurrency == CryptoCurrency.btcln
// FIXME amount estimation is broken/impossible for ln, konsti suggested this
? (amountParsingProxy.parseCryptoString(depositAvailableAmount, depositCurrency).toInt() -
10)
? (amountParsingProxy.parseCryptoString(depositAvailableAmount, depositCurrency) -
Money.fromInt(10, depositCurrency))
: await bitcoin!.estimateFakeSendAllTxAmount(
wallet,
priority,
@@ -1320,8 +1415,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
: UnspentCoinType.any,
);
changeDepositAmount(
amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true);
changeDepositAmount(amount: amount.toString(), isCanonical: true);
} else if (wallet.type == WalletType.monero) {
final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any);
@@ -1340,8 +1434,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return;
}
final balanceAmount = _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(
balanceForCurrency.fullAvailableBalance, balanceCurrency!);
final balanceAmount =
_appStore.amountParsingProxy.asDisplayString(balanceForCurrency.available);
final balanceDouble = double.tryParse(balanceAmount.replaceAll(',', '.')) ?? 0.0;
if (balanceDouble <= 0) {
changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true);
@@ -1369,7 +1463,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return;
}
final balanceWei = balanceForCurrency.fullAvailableBalance;
final balanceWei = balanceForCurrency.available.amount;
final feeWei = BigInt.parse(feeString);
final amountAfterFeeWei = balanceWei > feeWei ? balanceWei - feeWei : BigInt.zero;
changeDepositAmount(
@@ -1393,40 +1487,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
_depositAmount = tmpAmount;
}
void updateTemplate() => _exchangeTemplateStore.update();
void addTemplate(
{required String amount,
required String depositCurrency,
required String receiveCurrency,
required String provider,
required String depositAddress,
required String receiveAddress,
required String depositCurrencyTitle,
required String receiveCurrencyTitle}) =>
_exchangeTemplateStore.addTemplate(
amount: amount,
depositCurrency: depositCurrency,
receiveCurrency: receiveCurrency,
provider: provider,
depositAddress: depositAddress,
receiveAddress: receiveAddress,
depositCurrencyTitle: depositCurrencyTitle,
receiveCurrencyTitle: receiveCurrencyTitle);
void removeTemplate({required ExchangeTemplate template}) =>
_exchangeTemplateStore.remove(template: template);
void _onPairChange({bool clearBoth = false}) {
if (clearBoth) {
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
if (!hasAllAmount) isSendAllEnabled = false;
} else {
if (isFixedRateMode) {
_depositAmount = '';
_depositAmount = null;
} else {
_receiveAmount = '';
_receiveAmount = null;
}
}
bestRate = 0.0;
@@ -1527,8 +1597,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
void saveSelectedProviders() {
_depositAmount = '';
_receiveAmount = '';
_depositAmount = null;
_receiveAmount = null;
isFixedRateMode = false;
_defineIsReceiveAmountEditable();
bestRateProvider = null;
@@ -1,33 +1,36 @@
import 'dart:async';
import 'dart:io';
import "dart:async";
import "dart:io";
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/wallet_type_utils.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:bitbox_flutter/bitbox_flutter.dart' as sdk;
import 'package:mobx/mobx.dart';
import "package:bitbox_flutter/bitbox_flutter.dart" as sdk;
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
import "package:hash_wallet/evm/evm.dart";
import "package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
import "package:hash_wallet/wallet_type_utils.dart";
import "package:cw_core/hardware/hardware_wallet_service.dart";
import "package:cw_core/root_dir.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import "package:mobx/mobx.dart";
part 'bitbox_view_model.g.dart';
part "bitbox_view_model.g.dart";
class BitboxViewModel = BitboxViewModelBase with _$BitboxViewModel;
abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
late final sdk.BitboxManager bitboxManager;
BitboxViewModelBase() {
if (!Platform.isIOS && !isMoneroOnly) {
bitboxManager = sdk.BitboxManager();
}
}
late final sdk.BitboxManager bitboxManager;
BitboxHardwareWalletDevice? _connectedDevice;
Timer? _isConnectedTimer;
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.bitbox;
@@ -36,7 +39,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
bool isBleEnabled = false;
@override
bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth
@observable
bool isConnecting = false;
@override
bool get hasBluetooth => Platform.isIOS && false; // TODO(Konsti): remove when we enable bluetooth
@override
Future<void> updateBleState() async {}
@@ -46,39 +53,58 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => bitboxManager.devices
.then((devices) => devices.map((d) => BitboxHardwareWalletDevice(d)).toList());
.then((devices) => devices.map(BitboxHardwareWalletDevice.new).toList());
@override
Future<void> stopScanning() async {}
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
if (!(device is BitboxHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (device is! BitboxHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
try {
final appDocDir = await getAppDir();
await bitboxManager.connect(device.device);
printV("Got Connection", file: '${appDocDir.path}/error.txt');
printV("Got Connection", file: "${appDocDir.path}/error.txt");
await bitboxManager.initBitBox();
printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt');
printV("Bitbox initialized!", file: "${appDocDir.path}/error.txt");
await bitboxManager.channelHashVerify();
printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt');
_isConnecting = false;
printV("Bitbox channel-hash verified!", file: "${appDocDir.path}/error.txt");
_connectedDevice = device;
_isConnectedTimer = Timer.periodic(const Duration(seconds: 1), (_) => _isStillConnected());
isConnecting = false;
return true;
} catch (e) {
printV(e);
}
_isConnecting = false;
isConnecting = false;
_connectedDevice = null;
_isConnectedTimer?.cancel();
_isConnectedTimer = null;
return false;
}
bool _isConnecting = false;
@override
bool get isConnected => false;
bool isConnected(WalletType type) => _connectedDevice != null;
Future<void> _isStillConnected() async {
final devices = await getAllUsbDevices();
if (!devices.any((d) => d.name == _connectedDevice?.name)) {
_connectedDevice = null;
_isConnectedTimer?.cancel();
_isConnectedTimer = null;
}
}
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
@@ -96,17 +122,17 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
}
@override
Future<void> initWallet(WalletBase wallet) async {
Future<void> initWallet(WalletBase wallet) {
switch (wallet.type) {
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for bitbox');
throw Exception("Unexpected wallet type: ${wallet.type} for bitbox");
}
}
}
@@ -1,14 +1,16 @@
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import "package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
import "package:cw_core/hardware/hardware_wallet_service.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
abstract class HardwareWalletViewModel {
HardwareWalletType get hardwareWalletType;
bool get isConnected;
bool get isBleEnabled;
bool get hasBluetooth;
bool get isConnecting;
bool isConnected(WalletType type);
Future<void> updateBleState();
@@ -16,6 +18,8 @@ abstract class HardwareWalletViewModel {
Future<List<HardwareWalletDevice>> getAllUsbDevices();
Future<List<HardwareWalletDevice>> getConnectedBleDevices() async => [];
Future<void> stopScanning();
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type);
@@ -25,4 +29,6 @@ abstract class HardwareWalletViewModel {
Future<void> initWallet(WalletBase wallet);
String? interpretErrorCode(String error) => null;
Future<void> close() async {}
}
@@ -1,58 +1,68 @@
import 'dart:async';
import 'dart:io';
import "dart:async";
import "dart:io";
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/main.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/routes.dart';
import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/wallet_type_utils.dart';
import 'package:cw_core/hardware/device_connection_type.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/widgets.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk;
import 'package:mobx/mobx.dart';
import 'package:permission_handler/permission_handler.dart';
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
import "package:hash_wallet/evm/evm.dart";
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/main.dart";
import "package:hash_wallet/monero/monero.dart";
import "package:hash_wallet/routes.dart";
import "package:hash_wallet/src/screens/connect_device/connect_device_page.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
import "package:hash_wallet/wallet_type_utils.dart";
import "package:cw_core/hardware/device_connection_type.dart";
import "package:cw_core/hardware/hardware_wallet_service.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/widgets.dart";
import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk;
import "package:mobx/mobx.dart";
import "package:permission_handler/permission_handler.dart";
part 'ledger_view_model.g.dart';
part "ledger_view_model.g.dart";
class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel;
abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
LedgerViewModelBase(AppStore appStore) {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) {
_initBLE();
}
});
reaction((_) => appStore.wallet, (wallet) async {
if (wallet != null && wallet.type != _connectingWalletType) {
await close();
}
});
if (!Platform.isIOS) {
ledgerPlusUSB = sdk.LedgerInterface.usb();
}
}
}
late final sdk.LedgerInterface ledgerPlusBLE;
late final sdk.LedgerInterface ledgerPlusUSB;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero, HardwareWalletType.ledger, Platform.isIOS)
.isNotEmpty;
WalletType.monero,
HardwareWalletType.ledger,
Platform.isIOS,
).isNotEmpty;
}
return true;
}
LedgerViewModelBase() {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) _initBLE();
});
updateBleState();
if (!Platform.isIOS) {
ledgerPlusUSB = sdk.LedgerInterface.usb();
}
}
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger;
@@ -60,6 +70,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
@observable
bool isBleEnabled = false;
@override
@observable
bool isConnecting = false;
@override
bool get hasBluetooth => true;
@@ -69,9 +83,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
if (isBleEnabled && !_bleIsInitialized) {
ledgerPlusBLE = sdk.LedgerInterface.ble(
onPermissionRequest: (_) async {
if (Platform.isMacOS) return true;
if (Platform.isMacOS) {
return true;
}
Map<Permission, PermissionStatus> statuses = await [
final statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
@@ -79,7 +95,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
return statuses.values.where((status) => status.isDenied).isEmpty;
},
bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
);
_bleIsInitialized = true;
}
@@ -91,30 +107,51 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
final newState = bleState == sdk.AvailabilityState.poweredOn;
if (newState != isBleEnabled) isBleEnabled = newState;
if (newState != isBleEnabled) {
isBleEnabled = newState;
}
}
@override
Stream<HardwareWalletDevice> scanForBleDevices() =>
ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d));
ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new);
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => ledgerPlusUSB.devices
.then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList());
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList());
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) await ledgerPlusBLE.stopScanning();
if (!Platform.isIOS) await ledgerPlusUSB.stopScanning();
Future<List<HardwareWalletDevice>> getConnectedBleDevices() async {
if (!_bleIsInitialized) {
return const [];
}
return (await ledgerPlusBLE.devices).map(LedgerHardwareWalletDevice.new).toList();
}
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) {
await ledgerPlusBLE.stopScanning();
}
if (!Platform.isIOS) {
await ledgerPlusUSB.stopScanning();
}
}
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
if (!(device is LedgerHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (device is! LedgerHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
_connectingWalletType = type;
if (isConnected) {
if (isConnected(type)) {
try {
await _connection!.disconnect().catchError((_) {});
} catch (_) {}
@@ -123,33 +160,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
final ledger =
device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB;
if (_connectionChangeSubscription == null) {
_connectionChangeSubscription = ledger.deviceStateChanges.listen(_connectionChangeListener);
}
_connectionChangeSubscription ??=
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
try {
_connection = await ledger.connect(device.device);
_isConnecting = false;
isConnecting = false;
return true;
} catch (e) {
printV(e);
}
_isConnecting = false;
isConnecting = false;
return false;
}
StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
sdk.LedgerConnection? _connection;
bool _isConnecting = false;
WalletType? _connectingWalletType;
bool suppressMoneroReconnectPrompt = false;
void _connectionChangeListener(sdk.BleConnectionState event) {
printV('Ledger Device State Changed: $event');
if (event == sdk.BleConnectionState.disconnected && !_isConnecting) {
printV("Ledger Device State Changed: $event");
if (event == sdk.BleConnectionState.disconnected && !isConnecting) {
_connection = null;
if (_connectingWalletType == WalletType.monero) {
monero!.resetLedgerConnection();
if (suppressMoneroReconnectPrompt) {
return;
}
Navigator.of(navigatorKey.currentContext!).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
@@ -157,8 +197,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
hardwareWalletType: HardwareWalletType.ledger,
allowChangeWallet: true,
isReconnect: true,
onConnectDevice: (context, ledgerVM) async {
if (context.mounted) {
onConnectDevice: (context, ledgerVM) {
if (context.mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
@@ -169,32 +209,32 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
@override
bool get isConnected => _connection != null && !(_connection!.isDisconnected);
bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected;
sdk.LedgerConnection get connection => _connection!;
@override
Future<void> initWallet(WalletBase wallet) async {
Future<void> initWallet(WalletBase wallet) {
switch (wallet.type) {
case WalletType.monero:
return monero!.setLedgerConnection(wallet, connection);
case WalletType.bitcoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.litecoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for ledger');
throw Exception("Unexpected wallet type: ${wallet.type} for ledger");
}
}
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.monero:
return monero!.getLedgerHardwareWalletService(connection);
case WalletType.bitcoin:
return bitcoin!.getLedgerHardwareWalletService(connection, true);
case WalletType.litecoin:
@@ -213,16 +253,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
return error;
}
var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString());
final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString());
String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
if (errorCode.contains("6985")) {
return S.current.ledger_error_tx_rejected_by_user;
} else if (errorCode.contains("5515")) {
return S.current.ledger_error_device_locked;
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) {
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) {
return S.current.ledger_error_wrong_app;
}
return null;
}
@override
Future<void> close() async {
try {
printV("Resetting ledger VM");
await _connectionChangeSubscription?.cancel();
await _connection?.disconnect().catchError((_) {});
_connectingWalletType = null;
_connectionChangeSubscription = null;
_connection = null;
isConnecting = false;
await stopScanning();
} catch (_) {}
}
}
@@ -0,0 +1,521 @@
import "dart:async";
import "dart:convert";
import "dart:io";
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/core/secure_storage.dart";
import "package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
import "package:hash_wallet/evm/evm.dart";
import "package:hash_wallet/main.dart";
import "package:hash_wallet/monero/monero.dart";
import "package:hash_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart";
import "package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
import "package:hash_wallet/wallet_type_utils.dart";
import "package:cw_core/encryption_file_utils.dart";
import "package:cw_core/hardware/device_connection_type.dart";
import "package:cw_core/hardware/hardware_wallet_service.dart";
import "package:cw_core/key.dart";
import "package:cw_core/root_dir.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import "package:device_info_plus/device_info_plus.dart";
import "package:flutter/material.dart";
import "package:mobx/mobx.dart";
import "package:permission_handler/permission_handler.dart";
import "package:trezor_connect/trezor_connect.dart" as connect_sdk;
import "package:trezor_flutter/trezor_flutter.dart" as sdk;
part "trezor_connect_view_model.g.dart";
const trezorUseNative = [WalletType.bitcoin, WalletType.monero];
class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel;
abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store {
TrezorConnectViewModelBase(this.trezorConnect, this._secureStorage) {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) {
_initBLE();
}
});
if (!Platform.isIOS) {
trezorUSB = sdk.TrezorInterface.usb();
}
}
}
StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
final connect_sdk.TrezorConnect trezorConnect;
final SecureStorage _secureStorage;
late final sdk.TrezorInterface trezorBLE;
late final sdk.TrezorInterface trezorUSB;
sdk.ThpState? _state;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero,
HardwareWalletType.trezor,
Platform.isIOS,
).isNotEmpty;
}
return true;
}
bool _bleIsInitialized = false;
Future<void> _initBLE() async {
if (isBleEnabled && !_bleIsInitialized) {
trezorBLE = sdk.TrezorInterface.ble(
onPermissionRequest: (_) async {
if (Platform.isMacOS) {
return true;
}
final Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
].request();
return statuses.values.where((status) => status.isDenied).isEmpty;
},
bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
);
_bleIsInitialized = true;
}
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
@override
@observable
bool isConnecting = false;
@override
@observable
bool isBleEnabled = false;
@override
bool get hasBluetooth => true;
@override
Future<void> updateBleState() async {
final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState();
final newState = bleState == sdk.AvailabilityState.poweredOn;
if (newState != isBleEnabled) {
isBleEnabled = newState;
}
}
@override
Stream<HardwareWalletDevice> scanForBleDevices() =>
trezorBLE.scan().map(TrezorHardwareWalletDevice.new);
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
trezorUSB.devices.then((devices) => devices.map(TrezorHardwareWalletDevice.new).toList());
@override
Future<List<HardwareWalletDevice>> getConnectedBleDevices() async {
if (!_bleIsInitialized) {
return const [];
}
return (await trezorBLE.devices).map(TrezorHardwareWalletDevice.new).toList();
}
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) {
await trezorBLE.stopScanning();
}
if (!Platform.isIOS) {
await trezorUSB.stopScanning();
}
}
sdk.TrezorClient? _client;
Completer<String?>? _pinCompleter;
Completer<TrezorDeviceSettings>? _settingsCompleter;
/// Settings that successfully opened a session, kept in memory (never persisted) per wallet id
/// so a reconnect during a send doesn't ask for the wallet options / passphrase again.
final Map<String, TrezorDeviceSettings> _sessionSettings = {};
TrezorDeviceSettings? _lastUsedSettings;
String? _reconnectWalletId;
/// The wallet the live session (and with it the passphrase) belongs to, null if unknown
String? _sessionWalletId;
/// Call right before opening the connect page to reconnect the device for [wallet].
/// Only then the settings of the previous session are reused; creating or restoring a wallet
/// always asks for them.
void prepareReconnect(WalletBase wallet) => _reconnectWalletId = wallet.id;
/// Call when the connect page opened after [prepareReconnect] is closed, so an attempt that was
/// backed out of can never leak its settings into a later, unrelated connect (e.g. a restore).
void cancelReconnect() => _reconnectWalletId = null;
/// Call once a wallet was created / restored with the live session
void bindSessionToWallet(WalletBase wallet) {
_sessionWalletId = wallet.id;
final settings = _lastUsedSettings;
if (settings != null) {
_sessionSettings[wallet.id] = settings;
_lastUsedSettings = null;
}
}
/// False if the live session is known to belong to another wallet, e.g. one with a different
/// passphrase. Using it would only fail when signing.
bool isSessionFor(WalletBase wallet) =>
_sessionWalletId == null || _sessionWalletId == wallet.id;
Future<void> dropSession() async {
final client = _client;
_client = null;
_sessionWalletId = null;
_lastUsedSettings = null;
try {
await client?.connection.disconnect();
} catch (_) {}
}
@observable
TrezorParingState paringState = TrezorParingState.initial;
void setParingPin(String pin) => _pinCompleter?.complete(pin);
void setDeviceSettings(TrezorDeviceSettings settings) => _settingsCompleter?.complete(settings);
@override
@action
Future<bool> connectDevice(
HardwareWalletDevice device,
WalletType type, [
bool isRetry = false,
]) async {
if (device is! TrezorHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
paringState = TrezorParingState.initial;
final reconnectWalletId = _reconnectWalletId;
_reconnectWalletId = null;
final reusedSettings = reconnectWalletId != null ? _sessionSettings[reconnectWalletId] : null;
try {
final trezorInterface =
device.connectionType == HardwareWalletConnectionType.ble ? trezorBLE : trezorUSB;
final connection = await trezorInterface.connect(device.device);
_connectionChangeSubscription ??=
trezorInterface.deviceStateChanges.listen(_connectionChangeListener);
if (!isRetry) {
unawaited(
showModalBottomSheet(
context: navigatorKey.currentContext!,
isScrollControlled: true,
isDismissible: false,
enableDrag: false,
useSafeArea: true,
builder: (_) => HardwareWalletProceedOnDeviceSheet(
hardwareWalletType: hardwareWalletType,
trezorConnectVM: this,
onRetry: () => connectDevice(device, type, true),
),
),
);
}
Future<String> onPinCode() async {
_pinCompleter = Completer<String?>();
paringState = TrezorParingState.enterPin;
final res = await _pinCompleter!.future;
paringState = TrezorParingState.verifyingPin;
if (res == null) {
throw Exception();
}
return res;
}
final deviceInfo = await _deviceName;
_state ??= await _getState();
_client = sdk.TrezorClient.getClientForConnection(
connection,
_state!,
"Cake Wallet",
deviceInfo,
onPinCode,
);
await _client!.createChannel();
final hasAutoPairingCredentials =
_state!.pairingCredentials.any((c) => c.autoconnect == true);
final isAutoPairingAvailable = _client is sdk.TrezorClientV2 && !hasAutoPairingCredentials;
final TrezorDeviceSettings settings;
if (reusedSettings != null) {
settings = reusedSettings;
} else {
paringState =
TrezorParingState.awaitingSettings(isAutoPairingAvailable: isAutoPairingAvailable);
_settingsCompleter = Completer<TrezorDeviceSettings>();
settings = await _settingsCompleter!.future;
}
paringState = TrezorParingState.awaitingPassphrase;
if (settings.enableAutoParing && isAutoPairingAvailable) {
if (_client case final sdk.TrezorClientV2 clientV2) {
try {
final auto = await clientV2.getAutoPairingCredentials();
_state!.setPairingCredentials([auto]);
await _saveState();
} catch (_) {}
}
}
if ((settings.passphrase ?? "").isNotEmpty || settings.passphraseOnDevice) {
final passphrase = settings.passphraseOnDevice
? const sdk.TrezorPassphrase.onDevice()
: sdk.TrezorPassphrase.value(settings.passphrase ?? "");
await _client!.createSession(passphrase);
}
_lastUsedSettings = settings;
_sessionWalletId = reconnectWalletId;
paringState = TrezorParingState.success;
return true;
} catch (e) {
await _client?.connection.disconnect();
_client = null;
_sessionWalletId = null;
_lastUsedSettings = null;
// Drop the (possibly half way through a handshake) state, the next attempt reloads the
// saved pairing credentials instead of asking for the pairing code again.
_state = null;
if (reconnectWalletId != null) {
_sessionSettings.remove(reconnectWalletId);
}
// rethrow;
paringState = TrezorParingState.fail(e.toString());
printV(e);
return false;
} finally {
isConnecting = false;
_reconnectWalletId = null;
}
}
Future<String> get _deviceName async {
final deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
return (await deviceInfo.androidInfo).model;
}
if (Platform.isIOS) {
return (await deviceInfo.iosInfo).name;
}
return "Computer";
}
@override
bool isConnected(WalletType type) => trezorUseNative.contains(type)
? _client != null && _client?.connection.isDisconnected == false
: true;
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.monero:
return monero!.getTrezorHardwareWalletService(_client!);
case WalletType.bitcoin:
return bitcoin!.getTrezorHardwareWalletService(null, _client!, true);
case WalletType.litecoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, null, false);
case WalletType.ethereum:
case WalletType.polygon:
return evm!.getTrezorHardwareWalletService(trezorConnect);
default:
throw UnimplementedError();
}
}
@override
Future<void> initWallet(WalletBase wallet) async {
// Only a session that was opened for this wallet may hand its settings to it
final lastUsedSettings = _lastUsedSettings;
if (lastUsedSettings != null && _sessionWalletId == wallet.id) {
_sessionSettings[wallet.id] = lastUsedSettings;
_lastUsedSettings = null;
}
switch (wallet.type) {
case WalletType.monero:
return monero!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception("Unexpected wallet type: ${wallet.type} for trezor");
}
}
final EncryptionFileUtils _encryptionFileUtils = encryptionFileUtilsFor(true);
static const String _secureStorageKey = "com.cakewallet.trezor/thp_state";
Future<String> get _thpJsonFile async => "${(await getAppDir()).path}/thp_state.json.enc";
Future<sdk.ThpState> _getState() async {
final password = await _secureStorage.read(key: _secureStorageKey);
if (password == null) {
return _saveState();
}
try {
final state = await _encryptionFileUtils.read(path: await _thpJsonFile, password: password);
return sdk.ThpState.fromJson(state);
} catch (_) {
return _saveState();
}
}
Future<sdk.ThpState> _saveState() async {
try {
final password = generateKey();
await _secureStorage.write(key: _secureStorageKey, value: password);
final file = File(await _thpJsonFile);
if (!file.existsSync()) {
file.createSync();
}
final state = _state ?? sdk.ThpState();
await _encryptionFileUtils.write(
path: file.path,
password: password,
data: jsonEncode(state.toMap()),
);
_state = state;
return state;
} catch (_) {
throw Exception("Unable to save Trezor State");
}
}
Future<void> _connectionChangeListener(sdk.BleConnectionState event) async {
printV("Trezor Device State Changed: $event");
if (event != sdk.BleConnectionState.disconnected || isConnecting) {
return;
}
final client = _client;
if (client == null) {
return;
}
// The events aren't device specific, so check if it's actually our Trezor that is gone
if (client.connection.connectionType == sdk.ConnectionType.ble) {
try {
final state = await sdk.UniversalBle.getConnectionState(client.connection.device.id);
if (state == sdk.BleConnectionState.connected) {
return;
}
} catch (_) {}
}
if (identical(_client, client)) {
_client = null;
_sessionWalletId = null;
}
}
Future<bool> syncKeyImages(WalletBase wallet) async {
if (wallet.type == WalletType.monero) {
try {
await monero!.syncTrezor(wallet);
} catch (_) {
return false;
}
}
return true;
}
@override
Future<void> close() async {
try {
await _connectionChangeSubscription?.cancel();
_connectionChangeSubscription = null;
isConnecting = false;
await stopScanning();
} catch (_) {}
}
}
abstract class TrezorParingState {
static TrezorParingState initial = InitialTrezorParingState();
static TrezorParingState enterPin = EnterPinTrezorParingState();
static TrezorParingState success = SuccessTrezorParingState();
static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
static TrezorParingState awaitingPassphrase = AwaitingPassphraseTrezorParingState();
static TrezorParingState awaitingSettings({required bool isAutoPairingAvailable}) =>
AwaitingSettingsTrezorParingState(isAutoPairingAvailable: isAutoPairingAvailable);
static TrezorParingState fail(String message) => FailTrezorParingState(message);
}
class InitialTrezorParingState extends TrezorParingState {}
class EnterPinTrezorParingState extends TrezorParingState {}
class VerifyingPinTrezorParingState extends TrezorParingState {}
class AwaitingSettingsTrezorParingState extends TrezorParingState {
AwaitingSettingsTrezorParingState({required this.isAutoPairingAvailable});
final bool isAutoPairingAvailable;
}
class AwaitingPassphraseTrezorParingState extends TrezorParingState {}
class SuccessTrezorParingState extends TrezorParingState {}
class FailTrezorParingState extends TrezorParingState {
FailTrezorParingState(this.message);
final String message;
}
class TrezorDeviceSettings {
const TrezorDeviceSettings({
required this.enableAutoParing,
required this.passphraseOnDevice,
this.passphrase,
});
final bool enableAutoParing;
final bool passphraseOnDevice;
final String? passphrase;
}
@@ -1,80 +0,0 @@
import 'dart:async';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:trezor_connect/trezor_connect.dart' as sdk;
import 'package:mobx/mobx.dart';
part 'trezor_view_model.g.dart';
class TrezorViewModel = TrezorViewModelBase with _$TrezorViewModel;
abstract class TrezorViewModelBase extends HardwareWalletViewModel with Store {
final sdk.TrezorConnect trezorConnect;
TrezorViewModelBase(this.trezorConnect);
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
@override
@observable
bool isBleEnabled = false;
@override
bool get hasBluetooth => false;
@override
Future<void> updateBleState() async {}
@override
Stream<HardwareWalletDevice> scanForBleDevices() => throw UnimplementedError();
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() async => [];
@override
Future<void> stopScanning() async {}
@override
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async => true;
@override
bool get isConnected => true;
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.bitcoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
case WalletType.litecoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
case WalletType.ethereum:
case WalletType.polygon:
return evm!.getTrezorHardwareWalletService(trezorConnect);
default:
throw UnimplementedError();
}
}
@override
Future<void> initWallet(WalletBase wallet) async {
switch (wallet.type) {
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
}
}
}
@@ -9,6 +9,7 @@ import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/parse_fixed.dart';
import 'package:cw_core/pending_transaction.dart';
@@ -22,7 +23,10 @@ class DEuroViewModel = DEuroViewModelBase with _$DEuroViewModel;
abstract class DEuroViewModelBase with Store {
final AppStore _appStore;
static BigInt get MIN_ACCRUED_INTEREST => BigInt.parse("1000000000000");
static Money get MIN_ACCRUED_INTEREST =>
Money(BigInt.parse("1000000000000"), CryptoCurrency.deuro);
static Money get ZERO => Money.zero(CryptoCurrency.deuro);
DEuroViewModelBase(
this._appStore, this.balanceViewModel, this._settingsStore, this._fiatConversationStore,
@@ -51,13 +55,13 @@ abstract class DEuroViewModelBase with Store {
@computed
String get pendingTransactionFiatAmountFormatted {
final amount = transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted);
return isFiatDisabled ? '' : '$amount ${fiat.title}';
final amount = transaction == null ? "0.00" : _getDEuroFiatAmount(transaction!.amount);
return isFiatDisabled ? "" : "$amount ${fiat.symbol}";
}
@computed
String get pendingTransactionFeeFiatAmountFormatted {
var amount = '0.00';
var amount = "0.00";
try {
if (transaction != null) {
final feeCurrency = CryptoCurrency.eth;
@@ -67,42 +71,43 @@ abstract class DEuroViewModelBase with Store {
);
}
} catch (_) {}
return isFiatDisabled ? '' : '$amount ${fiat.title}';
return isFiatDisabled ? "" : "$amount ${fiat.symbol}";
}
@computed
String get accountBalanceFormated {
Money get accountBalance {
final dEuroKey = balanceViewModel.balances.keys
.firstWhereOrNull((e) => e.title == CryptoCurrency.deuro.title);
if (dEuroKey == null) return '0.00';
return balanceViewModel.balances[dEuroKey]?.availableBalance ?? '0.00';
if (dEuroKey == null) return ZERO;
return wallet.balance[dEuroKey]?.available ?? ZERO;
}
@observable
BigInt savingsBalance = BigInt.zero;
Money savingsBalance = ZERO;
@computed
String get savingsBalanceFormated =>
evm!.formatterEVMAmountToDouble(amount: savingsBalance).toStringAsFixed(6);
String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalance);
@observable
Money? savingsBalanceV1 = null;
@computed
String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalanceFormated);
String get fiatSavingsBalanceV1Formated => _getDEuroFiatAmount(savingsBalanceV1);
@observable
ExecutionState state = InitialExecutionState();
@observable
String interestRateFormated = '0';
String interestRateFormated = "0";
@observable
BigInt accruedInterest = BigInt.zero;
Money accruedInterest = ZERO;
@computed
String get accruedInterestFormated =>
evm!.formatterEVMAmountToDouble(amount: accruedInterest).toStringAsFixed(6);
String get accruedInterestFormated => accruedInterest.toStringWithPrecision(fractionalDigits: 6);
@computed
String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterestFormated);
String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterest);
@observable
BigInt approvedTokens = BigInt.zero;
@@ -128,8 +133,12 @@ abstract class DEuroViewModelBase with Store {
@action
Future<void> reloadSavingsUserData() async {
approvedTokens = await evm!.getDEuroSavingsApproved(_appStore.wallet!) ?? BigInt.zero;
savingsBalance = await evm!.getDEuroSavingsBalance(_appStore.wallet!) ?? BigInt.zero;
accruedInterest = await evm!.getDEuroAccruedInterest(_appStore.wallet!) ?? BigInt.zero;
savingsBalance = await evm!.getDEuroSavingsBalance(_appStore.wallet!) ?? ZERO;
accruedInterest = await evm!.getDEuroAccruedInterest(_appStore.wallet!) ?? ZERO;
final v1Balance = await evm!.getDEuroSavingsV1Balance(_appStore.wallet!) ?? ZERO;
savingsBalanceV1 = v1Balance.isZero ? null : v1Balance;
isLoading = false;
}
@@ -151,9 +160,8 @@ abstract class DEuroViewModelBase with Store {
state = TransactionCommitting();
final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!;
final approval = await evm!.enableDEuroSaving(_appStore.wallet!, priority);
if (approval == null) {
throw Exception('DEuro saving not available');
}
if (approval == null) throw Exception("DEuro saving not available");
approvalTransaction = approval;
state = InitialExecutionState();
} catch (e) {
@@ -167,13 +175,13 @@ abstract class DEuroViewModelBase with Store {
state = TransactionCommitting();
if (amountRaw.isEmpty || amountRaw.trim().isEmpty) {
throw Exception('Invalid amount: amount cannot be empty');
throw Exception("Invalid amount: amount cannot be empty");
}
final amount = tryParseFixed(amountRaw, 18);
if (amount == BigInt.zero || amount == null) {
throw Exception('Invalid amount: amount cannot be zero');
throw Exception("Invalid amount: amount cannot be zero");
}
final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!;
@@ -181,9 +189,24 @@ abstract class DEuroViewModelBase with Store {
final tx = await (isAdding
? evm!.addDEuroSaving(_appStore.wallet!, amount, priority)
: evm!.removeDEuroSaving(_appStore.wallet!, amount, priority));
if (tx == null) {
throw Exception('DEuro saving not available');
}
if (tx == null) throw Exception("DEuro saving not available");
transaction = tx;
state = InitialExecutionState();
} catch (e) {
state = FailureState(e.toString());
}
}
@action
Future<void> prepareSavingsV1Withdraw() async {
try {
state = TransactionCommitting();
actionType = DEuroActionType.withdraw;
final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!;
final tx = await evm!.withdrawDEuroSavingV1(_appStore.wallet!, priority);
if (tx == null) throw Exception("DEuro saving not available");
transaction = tx;
state = InitialExecutionState();
} catch (e) {
@@ -193,13 +216,13 @@ abstract class DEuroViewModelBase with Store {
Future<void> prepareCollectInterest() async {
if (accruedInterest < MIN_ACCRUED_INTEREST) {
state = FailureState('Accrued interest is below minimum threshold');
state = FailureState("Accrued interest is below minimum threshold");
return;
}
final formatted = accruedInterestFormated;
if (formatted.isEmpty || formatted == '0.000000') {
state = FailureState('Invalid accrued interest amount');
if (formatted.isEmpty || formatted == "0.000000") {
state = FailureState("Invalid accrued interest amount");
return;
}
@@ -212,9 +235,8 @@ abstract class DEuroViewModelBase with Store {
actionType = DEuroActionType.reinvest;
final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!;
final tx = await evm!.reinvestDEuroInterest(_appStore.wallet!, priority);
if (tx == null) {
throw Exception('DEuro saving not available');
}
if (tx == null) throw Exception("DEuro saving not available");
transaction = tx;
state = InitialExecutionState();
} catch (e) {
@@ -261,7 +283,9 @@ abstract class DEuroViewModelBase with Store {
state = InitialExecutionState();
}
String _getDEuroFiatAmount(String amount) {
String _getDEuroFiatAmount(Money? amount) {
if (amount == null) return "0.00";
try {
var dEuro = CryptoCurrency.deuro;
final keys = _fiatConversationStore.prices.keys.toList();
@@ -270,19 +294,14 @@ abstract class DEuroViewModelBase with Store {
}
return calculateFiatAmount(
price: _fiatConversationStore.prices[dEuro]!,
cryptoAmount: amount,
cryptoAmount: amount.toStringWithPrecision(fractionalDigits: 6),
);
} catch (_) {
return '0.00';
return "0.00";
}
}
}
class NoEtherState extends ExecutionState {}
enum DEuroActionType {
deposit,
withdraw,
reinvest,
none;
}
enum DEuroActionType { deposit, withdraw, reinvest, none }
+23 -4
View File
@@ -25,6 +25,10 @@ class LinkViewModel {
Uri? currentLink;
bool readyToOpenPage = false;
final StreamController<Uri> _incomingPaymentLinksController = StreamController<Uri>.broadcast();
Stream<Uri> get incomingPaymentLinks => _incomingPaymentLinksController.stream;
bool get _isValidPaymentUri => currentLink?.path.isNotEmpty ?? false;
bool get isWalletConnectLink => currentLink?.authority == 'wc';
bool get isNanoGptLink => currentLink?.scheme == 'nano-gpt';
@@ -34,7 +38,7 @@ class LinkViewModel {
String? getRouteToGo() {
if (isWalletConnectLink) {
if (!isWalletConnectCompatibleChain(appStore.wallet!.type)) {
_errorToast(S.current.switchToEVMCompatibleWallet);
_errorToast(S.current.switchToWCCompatibleWallet(walletConnectCompatibleChainsLabel()));
return null;
}
return Routes.walletConnectConnectionsListing;
@@ -47,7 +51,7 @@ class LinkViewModel {
case 'send':
return Routes.send;
case 'receive':
return Routes.addressPage;
return Routes.newReceivePage;
default:
return null;
}
@@ -96,7 +100,10 @@ class LinkViewModel {
}
if (_isValidPaymentUri) {
return {"paymentRequest": PaymentRequest.fromUri(currentLink)};
return {
"paymentRequest": PaymentRequest.fromUri(currentLink),
"rawLink": currentLink.toString(),
};
}
return null;
@@ -132,8 +139,20 @@ class LinkViewModel {
return;
}
// Prevent navigating to the same route again.
if (route == Routes.send &&
!isQuickActionLink &&
currentLink != null &&
_incomingPaymentLinksController.hasListener) {
final link = currentLink!;
currentLink = null;
_incomingPaymentLinksController.add(link);
return;
}
// Prevent navigating to the same route again and clear the link so a later auth
// event cannot refire it.
if (appStore.currentRouteName == route) {
currentLink = null;
return;
}
@@ -1,8 +0,0 @@
class AccountListItem {
AccountListItem({required this.label, required this.id, this.balance, this.isSelected = false});
final String label;
final int id;
final bool isSelected;
final String? balance;
}
@@ -1,89 +0,0 @@
import 'package:hash_wallet/entities/balance_display_mode.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hash_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:hash_wallet/monero/monero.dart';
part 'monero_account_list_view_model.g.dart';
class MoneroAccountListViewModel = MoneroAccountListViewModelBase with _$MoneroAccountListViewModel;
abstract class MoneroAccountListViewModelBase with Store {
MoneroAccountListViewModelBase(this._wallet, this.settingsStore) : scrollOffsetFromTop = 0;
final SettingsStore settingsStore;
@observable
double scrollOffsetFromTop;
@action
void setScrollOffsetFromTop(double scrollOffsetFromTop) {
this.scrollOffsetFromTop = scrollOffsetFromTop;
}
CryptoCurrency get currency => _wallet.currency;
@computed
List<AccountListItem> get accounts {
final hideBalance = settingsStore.balanceDisplayMode == BalanceDisplayMode.hiddenBalance;
if (_wallet.type == WalletType.monero) {
return monero!
.getAccountList(_wallet)
.accounts
.map((acc) => AccountListItem(
label: acc.label,
id: acc.id,
balance: hideBalance ? '●●●●●●' : acc.balance,
isSelected: acc.id == monero!.getCurrentAccount(_wallet).id))
.toList();
}
if (_wallet.type == WalletType.wownero) {
return wownero!
.getAccountList(_wallet)
.accounts
.map((acc) => AccountListItem(
label: acc.label,
id: acc.id,
balance: hideBalance ? '●●●●●●' : acc.balance,
isSelected: acc.id == wownero!.getCurrentAccount(_wallet).id))
.toList();
}
throw Exception('Unexpected wallet type: ${_wallet.type} for monero');
}
@computed
AccountListItem get selected {
final currentId = _wallet.type == WalletType.wownero
? wownero!.getCurrentAccount(_wallet).id
: monero!.getCurrentAccount(_wallet).id;
return accounts.firstWhere((item) => item.id == currentId);
}
final WalletBase _wallet;
void select(AccountListItem item) {
if (_wallet.type == WalletType.monero) {
monero!.setCurrentAccount(
_wallet,
item.id,
item.label,
item.balance,
);
}
if (_wallet.type == WalletType.wownero) {
wownero!.setCurrentAccount(
_wallet,
item.id,
item.label,
item.balance,
);
}
}
}
@@ -3,12 +3,10 @@ import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:hash_wallet/entities/qr_scanner.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/utils/permission_handler.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -17,8 +15,8 @@ part 'node_create_or_edit_view_model.g.dart';
class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase with _$NodeCreateOrEditViewModel;
abstract class NodeCreateOrEditViewModelBase with Store {
NodeCreateOrEditViewModelBase(
this._nodeSource, this.walletType, this.editingNode, this._settingsStore)
NodeCreateOrEditViewModelBase(this.isPow, this.walletType, this._settingsStore,
{this.editingNode})
: state = InitialExecutionState(),
connectionState = InitialExecutionState(),
label = editingNode?.label ?? '',
@@ -106,6 +104,12 @@ abstract class NodeCreateOrEditViewModelBase with Store {
@observable
String socksProxyAddress;
@observable
Node? editingNode;
@observable
bool isPow;
@computed
bool get isReady => address.isNotEmpty;
@@ -144,8 +148,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
}
final WalletType walletType;
final Node? editingNode;
final Box<Node> _nodeSource;
final SettingsStore _settingsStore;
void updateViewModelFromText(String key, String value) {
@@ -233,30 +235,29 @@ abstract class NodeCreateOrEditViewModelBase with Store {
void setSocksProxyAddress(String val) => socksProxyAddress = val;
@action
Future<void> save({Node? editingNode, bool saveAsCurrent = false}) async {
final node = Node(
label: label,
uri: uri,
path: path,
type: walletType,
login: login,
password: password,
useSSL: useSSL,
trusted: trusted,
isEnabledForAutoSwitching: isEnabledForAutoSwitching,
socksProxyAddress: socksProxyAddress);
Future<void> delete({required Node editingNode}) async {
await editingNode.delete();
}
@action
Future<void> save({bool saveAsCurrent = false}) async {
editingNode ??= Node();
editingNode!.type = walletType;
editingNode!.label = label;
editingNode!.uriRaw = uri;
editingNode!.path = path;
editingNode!.login = login;
editingNode!.password = password;
editingNode!.isPow = isPow;
editingNode!.useSSL = useSSL;
editingNode!.trusted = trusted;
editingNode!.socksProxyAddress = socksProxyAddress;
try {
state = IsExecutingState();
if (editingNode != null) {
await _nodeSource.put(editingNode.key, node);
} else if (_existingNode(node) != null) {
setAsCurrent(_existingNode(node)!);
} else {
await _nodeSource.add(node);
setAsCurrent(_nodeSource.values.last);
}
await editingNode!.save();
if (saveAsCurrent) {
setAsCurrent(node);
setAsCurrent(editingNode!);
}
state = ExecutedSuccessfullyState();
@@ -286,16 +287,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
}
}
Node? _existingNode(Node node) {
final nodes = _nodeSource.values.toList();
nodes.forEach((item) {
item.login ??= '';
item.password ??= '';
item.useSSL ??= false;
});
return nodes.firstWhereOrNull((item) => item == node);
}
@action
void setAsCurrent(Node node) => _settingsStore.nodes[walletType] = node;
@@ -1,13 +1,14 @@
import 'dart:math';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/utils/mobx.dart';
import 'package:cw_core/node_list.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:cw_core/node.dart';
import 'package:hash_wallet/entities/node_list.dart';
import 'package:hash_wallet/entities/default_settings_migration.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hash_wallet/evm/evm.dart';
@@ -15,16 +16,38 @@ import 'package:hash_wallet/reactions/wallet_connect.dart';
part 'node_list_view_model.g.dart';
class NodeSpeed {
final String iconPath;
final String darkIconPath;
// fraction of best node ping needed to be classified as this speed.
final double treshold;
const NodeSpeed._(this.iconPath, this.darkIconPath, this.treshold);
static const disconnected = NodeSpeed._("assets/new-ui/node_speed_badges/disconnected.svg",
"assets/new-ui/node_speed_badges/disconnected-dark.svg", 0);
static const slow = NodeSpeed._("assets/new-ui/node_speed_badges/slow.svg",
"assets/new-ui/node_speed_badges/slow-dark.svg", 0);
static const medium = NodeSpeed._("assets/new-ui/node_speed_badges/medium.svg",
"assets/new-ui/node_speed_badges/medium.svg", 0.5);
static const fast = NodeSpeed._(
"assets/new-ui/node_speed_badges/fast.svg", "assets/new-ui/node_speed_badges/fast.svg", 0.8);
static const all = [fast, medium, slow, disconnected];
}
class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel;
abstract class NodeListViewModelBase with Store {
NodeListViewModelBase(this._nodeSource, this._appStore)
NodeListViewModelBase(this._appStore, this.isPow)
: nodes = ObservableList<Node>(),
_nodeSpeeds = ObservableMap<String, int>(),
settingsStore = _appStore.settingsStore {
_bindNodes();
bindNodes();
reaction((_) => _appStore.wallet, (WalletBase? _wallet) {
_bindNodes();
bindNodes();
});
reaction((_) {
@@ -35,7 +58,7 @@ abstract class NodeListViewModelBase with Store {
}
return null;
}, (_) {
_bindNodes();
bindNodes();
});
}
@@ -53,7 +76,7 @@ abstract class NodeListViewModelBase with Store {
return settingsStore.getCurrentNode(walletType, chainId: chainId);
}
final node = settingsStore.nodes[walletType];
final node = isPow ? settingsStore.powNodes[walletType] : settingsStore.nodes[walletType];
if (node == null) {
throw Exception('No node for wallet type: $walletType');
}
@@ -73,24 +96,74 @@ abstract class NodeListViewModelBase with Store {
final ObservableList<Node> nodes;
final SettingsStore settingsStore;
final Box<Node> _nodeSource;
final AppStore _appStore;
final bool isPow;
final ObservableMap<String, int> _nodeSpeeds;
@computed
List<Node> get nonCurrentNodes => nodes.where((item) => item != currentNode).toList();
@observable
bool isTestingNodeSpeed = false;
Future<void> speedTestNodes() async {
try {
isTestingNodeSpeed = true;
_nodeSpeeds.clear();
final nodes = this.nodes.toList();
await Future.wait(nodes.map((node) async {
final sw = Stopwatch()..start();
final res =
await node.requestNode().timeout(const Duration(seconds: 10), onTimeout: () => false);
sw.stop();
if (res) {
_nodeSpeeds[node.uriRaw] = sw.elapsedMilliseconds;
}
}));
} finally {
isTestingNodeSpeed = false;
}
}
NodeSpeed? nodeSpeedFor(Node node) {
final bestSpeed = _bestNodeSpeed;
final currentSpeed = _nodeSpeeds[node.uriRaw];
if (bestSpeed == null) {
return null;
}
if (currentSpeed == null) {
return NodeSpeed.disconnected;
}
for (final speed in NodeSpeed.all) {
if (currentSpeed < (bestSpeed / speed.treshold)) return speed;
}
return null;
}
int? get _bestNodeSpeed {
try {
printV(_nodeSpeeds);
return _nodeSpeeds.values.reduce(min);
} catch (e) {}
return null;
}
Future<void> reset() async {
await resetToDefault(_nodeSource);
await resetToDefault();
final wallet = _appStore.wallet!;
final walletType = wallet.type;
Node node;
if (walletType == WalletType.bitcoin && wallet.isTestnet) {
node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!;
node = (await getBitcoinTestnetDefaultElectrumServer())!;
} else if (isEVMCompatibleChain(walletType)) {
final chainId = evm!.getSelectedChainId(wallet);
if (chainId != null) {
final nodeWalletType = evm!.getWalletTypeByChainId(chainId);
if (nodeWalletType != null) {
node = getDefaultNode(nodes: _nodeSource, type: nodeWalletType)!;
node = (await Node.getDefaultForWalletType(nodeWalletType))!;
} else {
throw Exception(
'Cannot reset node for EVM wallet: wallet type not found for chainId: $chainId');
@@ -99,7 +172,7 @@ abstract class NodeListViewModelBase with Store {
throw Exception('Cannot reset node for EVM wallet: chainId is null');
}
} else {
node = getDefaultNode(nodes: _nodeSource, type: walletType)!;
node = (await Node.getDefaultForWalletType(_appStore.wallet!.type))!;
}
await setAsCurrent(node);
@@ -126,11 +199,15 @@ abstract class NodeListViewModelBase with Store {
}
// For non-EVM wallets, use the wallet type directly
settingsStore.nodes[walletType] = node;
if (isPow) {
settingsStore.powNodes[walletType] = node;
} else {
settingsStore.nodes[walletType] = node;
}
}
@action
void _bindNodes() {
Future<void> bindNodes() async {
nodes.clear();
final wallet = _appStore.wallet!;
final walletType = wallet.type;
@@ -141,11 +218,8 @@ abstract class NodeListViewModelBase with Store {
if (chainId != null) {
final nodeWalletType = evm!.getWalletTypeByChainId(chainId);
if (nodeWalletType != null) {
_nodeSource.bindToList(
nodes,
filter: (val) => val.type == nodeWalletType,
initialFire: true,
);
nodes.addAll(await Node.getAllForWalletType(nodeWalletType));
return;
}
}
@@ -154,10 +228,8 @@ abstract class NodeListViewModelBase with Store {
}
// For non-EVM wallets, use the wallet type directly
_nodeSource.bindToList(
nodes,
filter: (val) => val.type == walletType,
initialFire: true,
);
nodes.addAll(isPow
? await Node.getAllForWalletTypePow(walletType)
: await Node.getAllForWalletType(walletType));
}
}
@@ -1,87 +1 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/utils/mobx.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:cw_core/node.dart';
import 'package:hash_wallet/entities/node_list.dart';
import 'package:hash_wallet/entities/default_settings_migration.dart';
import 'package:cw_core/wallet_type.dart';
part 'pow_node_list_view_model.g.dart';
class PowNodeListViewModel = PowNodeListViewModelBase with _$PowNodeListViewModel;
abstract class PowNodeListViewModelBase with Store {
PowNodeListViewModelBase(this._nodeSource, this._appStore)
: nodes = ObservableList<Node>(),
settingsStore = _appStore.settingsStore {
_bindNodes();
reaction((_) => _appStore.wallet, (WalletBase? _wallet) {
_bindNodes();
});
}
@computed
Node get currentNode {
final node = settingsStore.powNodes[_appStore.wallet!.type];
if (node == null) {
throw Exception('No node for wallet type: ${_appStore.wallet!.type}');
}
return node;
}
String getAlertContent(String uri) =>
S.current.change_current_node(uri) +
'${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance!.enabled ? '' : S.current.orbot_running_alert) : ''}';
@computed
bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching;
@action
void setEnableAutomaticNodeSwitching(bool value) =>
settingsStore.enableAutomaticNodeSwitching = value;
final ObservableList<Node> nodes;
final SettingsStore settingsStore;
final Box<Node> _nodeSource;
final AppStore _appStore;
Future<void> reset() async {
await resetPowToDefault(_nodeSource);
Node node;
switch (_appStore.wallet!.type) {
case WalletType.nano:
node = getNanoDefaultPowNode(nodes: _nodeSource)!;
break;
default:
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type} for pow node list');
}
await setAsCurrent(node);
}
@action
Future<void> delete(Node node) async => node.delete();
Future<void> setAsCurrent(Node node) async =>
settingsStore.powNodes[_appStore.wallet!.type] = node;
@action
void _bindNodes() {
nodes.clear();
_nodeSource.bindToList(
nodes,
filter: (val) => val.type == _appStore.wallet!.type,
initialFire: true,
);
}
}
@@ -108,10 +108,10 @@ abstract class PayjoinDetailsViewModelBase with Store {
value: '${transactionInfo!.height}',
key: ValueKey('standard_list_item_transaction_details_height_key'),
),
if (transactionInfo!.feeFormatted()?.isNotEmpty ?? false)
if (transactionInfo!.fee != null)
StandartListItem(
title: S.current.transaction_details_fee,
value: transactionInfo!.feeFormatted()!,
value: transactionInfo!.fee!.toStringWithSymbol(),
key: ValueKey('standard_list_item_transaction_details_fee_key'),
),
]);
+31 -13
View File
@@ -41,7 +41,7 @@ abstract class PaymentViewModelBase with Store {
return getChainIdByCryptoCurrency(_lastDetectionResult!.detectedCurrency!);
}
return evm!.getChainIdByWalletType(detectedWalletType!);
return evm?.getChainIdByWalletType(detectedWalletType!);
}
@observable
@@ -52,7 +52,7 @@ abstract class PaymentViewModelBase with Store {
@computed
int? get currentChainId {
if (!isEVMCompatibleChain(currentWalletType)) return null;
if (!isEVMCompatibleChain(currentWalletType) || evm == null) return null;
return evm!.getSelectedChainId(appStore.wallet!);
}
@@ -74,12 +74,18 @@ abstract class PaymentViewModelBase with Store {
return PaymentFlowResult.incompatible('Unable to detect address type');
}
if (isEVMCompatibleChain(detectedWalletType!) && evm == null) {
return PaymentFlowResult.incompatible(
'Ethereum-compatible addresses are not supported in this app build.',
);
}
final currentWallet = appStore.wallet;
if (currentWallet != null &&
currentWallet.type == detectedWalletType &&
!isEVMCompatibleChain(detectedWalletType!)) {
return PaymentFlowResult.currentWalletCompatible();
return PaymentFlowResult.currentWalletCompatible(detectionResult);
}
if (isEVMCompatibleChain(detectedWalletType!)) {
@@ -106,7 +112,7 @@ abstract class PaymentViewModelBase with Store {
@action
Future<void> selectChain() async {
if (detectedWalletType == null) return;
if (detectedWalletType == null || evm == null || detectedChainId == null) return;
final node =
appStore.settingsStore.getCurrentNode(detectedWalletType!, chainId: detectedChainId);
@@ -114,6 +120,13 @@ abstract class PaymentViewModelBase with Store {
await evm!.selectChain(appStore.wallet!, detectedChainId!, node: node);
}
void applyManualEvmSelection(AddressDetectionResult detection) {
runInAction(() {
_lastDetectionResult = detection;
detectedWalletType = detection.detectedWalletType;
});
}
Future<List<WalletInfo>> getWalletsByType(WalletType walletType) async {
return (await WalletInfo.getAll()).where((wallet) => wallet.type == walletType).toList();
}
@@ -155,7 +168,7 @@ class PaymentFlowResult {
chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!);
}
if (chainId == null && addressDetectionResult.detectedWalletType != null) {
chainId = evm!.getChainIdByWalletType(addressDetectionResult.detectedWalletType!);
chainId = evm?.getChainIdByWalletType(addressDetectionResult.detectedWalletType!);
}
return PaymentFlowResult._(
@@ -169,8 +182,12 @@ class PaymentFlowResult {
}
/// Current wallet is compatible
factory PaymentFlowResult.currentWalletCompatible() =>
PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
factory PaymentFlowResult.currentWalletCompatible(
AddressDetectionResult addressDetectionResult) =>
PaymentFlowResult._(
type: PaymentFlowType.currentWalletCompatible,
addressDetectionResult: addressDetectionResult,
);
/// Single compatible wallet available
factory PaymentFlowResult.singleWallet(
@@ -181,8 +198,9 @@ class PaymentFlowResult {
if (chainId == null && addressDetectionResult.detectedCurrency != null) {
chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!);
}
if (chainId == null) {
chainId = evm!.getChainIdByWalletType(wallet.type);
if (chainId == null && isEVMCompatibleChain(wallet.type)) {
chainId = evm?.getChainIdByWalletType(wallet.type);
}
return PaymentFlowResult._(
@@ -203,8 +221,8 @@ class PaymentFlowResult {
if (chainId == null && addressDetectionResult.detectedCurrency != null) {
chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!);
}
if (chainId == null) {
chainId = evm!.getChainIdByWalletType(wallets.first.type);
if (chainId == null && isEVMCompatibleChain(wallets.first.type)) {
chainId = evm?.getChainIdByWalletType(wallets.first.type);
}
return PaymentFlowResult._(
@@ -225,8 +243,8 @@ class PaymentFlowResult {
if (chainId == null && addressDetectionResult.detectedCurrency != null) {
chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!);
}
if (chainId == null) {
chainId = evm!.getChainIdByWalletType(walletType);
if (chainId == null && isEVMCompatibleChain(walletType)) {
chainId = evm?.getChainIdByWalletType(walletType);
}
return PaymentFlowResult._(
+8
View File
@@ -1,4 +1,5 @@
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -32,6 +33,13 @@ abstract class RescanViewModelBase with Store {
@computed
bool get isMwebScan => wallet.type == WalletType.litecoin;
Future<int?> get savedRestoreHeight async => switch (wallet.type) {
WalletType.monero => wallet.walletInfo.restoreHeight != 0
? wallet.walletInfo.restoreHeight
: monero!.getRestoreHeight(wallet),
_ => null,
};
Future<bool> get isBitcoinMempoolAPIEnabled async =>
wallet.type == WalletType.bitcoin && await bitcoin!.checkIfMempoolAPIIsEnabled(wallet);
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:hash_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:hash_wallet/core/seed_validator.dart';
import 'package:hash_wallet/entities/parse_address_from_domain.dart';
import 'package:hash_wallet/entities/qr_scanner.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/routes.dart';
@@ -11,6 +11,7 @@ import 'package:hash_wallet/view_model/restore/restore_mode.dart';
import 'package:hash_wallet/view_model/restore/restore_wallet.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:flutter/cupertino.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:collection/collection.dart';
@@ -74,11 +75,9 @@ class WalletRestoreFromQRCode {
static String? _extractAddressFromUrl(String rawString, WalletType type) {
try {
return AddressResolver.extractAddressByType(
return AddressResolverUtils.extractAddressByType(
raw: rawString,
type: walletTypeToCryptoCurrency(
type,
),
type: walletTypeToCryptoCurrency(type),
requireSurroundingWhitespaces: false,
);
} catch (_) {
@@ -114,8 +113,8 @@ class WalletRestoreFromQRCode {
final prefix = code.startsWith('xpub')
? 'xpub'
: code.startsWith('zpub')
? 'zpub'
: '????';
? 'zpub'
: '????';
if (walletType == null) {
await _specifyWalletAssets(context, "Can't determine wallet type, please pick it manually");
walletType =
@@ -127,8 +126,8 @@ class WalletRestoreFromQRCode {
formattedUri = seedPhrase != null
? '$walletType:?seed=$seedPhrase'
: code.startsWith(prefix)
? '$walletType:?$prefix=$code'
: throw Exception('Failed to determine valid seed phrase');
? '$walletType:?$prefix=$code'
: throw Exception('Failed to determine valid seed phrase');
} else {
final index = code.indexOf(':');
final query = code.substring(index + 1).replaceAll('?', '&');
@@ -189,9 +188,15 @@ class WalletRestoreFromQRCode {
seedValue.split(' ').forEach((element) {
if (!words.contains(element)) {
throw Exception(
"Unexpected restore mode: mnemonic_seed is invalid or doesn't match wallet type");
"Unexpected restore mode: mnemonic_seed is invalid or doesn't match wallet type",
);
}
});
if (isEVMCompatibleChain(type) && !bip39.validateMnemonic(seedValue)) {
throw Exception(
'EVM mnemonic has an invalid checksum. Please check the seed phrase for typos.',
);
}
return WalletRestoreMode.seed;
}
@@ -239,12 +244,14 @@ class WalletRestoreFromQRCode {
Future<void> _specifyWalletAssets(BuildContext context, String error) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.error,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.error,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
@@ -32,7 +32,7 @@ abstract class RestoreFromBackupViewModelBase with Store {
void reset() => filePath = '';
@action
Future<void> import(String password) async {
Future<void> import(String password, {bool checkBackupApp = true}) async {
try {
state = IsExecutingState();
@@ -44,7 +44,9 @@ abstract class RestoreFromBackupViewModelBase with Store {
final file = File(filePath);
try {
await backupService.importBackupFile(file, password);
await backupService.importBackupFile(file, password, checkBackupApp: checkBackupApp);
} on IncompatibleBackupAppException {
rethrow;
} catch (e, s) {
if (e.toString().contains("unknown_backup_version")) {
state = FailureState(
@@ -63,7 +65,7 @@ abstract class RestoreFromBackupViewModelBase with Store {
final store = getIt.get<AppStore>();
ReactionDisposer? reaction;
await store.settingsStore.reload(nodeSource: getIt.get<Box<Node>>());
await store.settingsStore.reload();
await store.themeStore.loadSavedTheme(isFromBackup: true);
reaction = autorun((_) {
final wallet = store.wallet;
@@ -75,6 +77,9 @@ abstract class RestoreFromBackupViewModelBase with Store {
});
state = ExecutedSuccessfullyState();
} on IncompatibleBackupAppException {
state = InitialExecutionState();
rethrow;
} catch (e, s) {
var msg = e.toString().toLowerCase();
+85 -94
View File
@@ -1,25 +1,24 @@
import 'dart:math' show min;
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/di.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount_raw.dart';
import 'package:hash_wallet/entities/contact_base.dart';
import 'package:hash_wallet/entities/parse_address_from_domain.dart';
import 'package:hash_wallet/entities/parsed_address.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/screens/send/widgets/extract_address_from_parsed.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:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/format_fixed.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/print_verbose.dart';
@@ -45,12 +44,13 @@ abstract class OutputBase with Store {
note = '',
memo = "",
extractedAddress = '',
estimatedFee = '0.0',
parsedAddress = ParsedAddress(addresses: []) {
estimatedFee = Money.zero(cryptoCurrencyHandler()),
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) {
autorun((_) {
final status = _wallet.syncStatus;
printV("Sync status changed to $status. Recalculating fees");
if (status is! SyncedSyncStatus) {
return;
}
calculateEstimatedFee();
});
}
@@ -59,6 +59,9 @@ abstract class OutputBase with Store {
bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler());
@observable
bool isFiatEntry = false;
@observable
String fiatAmount;
@@ -69,8 +72,7 @@ abstract class OutputBase with Store {
String? displayName;
@computed
String get displayCryptoAmount =>
_appStore.amountParsingProxy.getDisplayCryptoAmount(cryptoAmount, cryptoCurrencyHandler());
String get displayCryptoAmount => _appStore.amountParsingProxy.asDisplayString(cryptoAmountMoney);
@observable
String cryptoFullBalance;
@@ -95,14 +97,13 @@ abstract class OutputBase with Store {
@computed
bool get isParsedAddress =>
parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty;
parsedAddress.addressSource != AddressSource.notParsed && parsedAddress.handle.isNotEmpty;
String roundedCryptoAmount(int digits) => displayCryptoAmount.withMaxDecimals(digits);
String roundedFiatAmount(int digits) {
if (fiatAmount.split(".").last.length <= digits) {
return fiatAmount;
}
if (fiatAmount.split(".").last.length <= digits) return fiatAmount;
return double.tryParse(fiatAmount.replaceAll(",", ""))?.toStringAsPrecision(digits) ?? '0.0';
}
@@ -110,52 +111,18 @@ abstract class OutputBase with Store {
String? stealthAddress;
@computed
int get formattedCryptoAmount {
int amount = 0;
Money get cryptoAmountMoney {
if (cryptoAmount.isEmpty) return Money.zero(cryptoCurrencyHandler());
try {
if (cryptoAmount.isNotEmpty) {
final _cryptoAmount = cryptoAmount.replaceAll(',', '.');
int _amount = 0;
switch (walletType) {
case WalletType.monero:
_amount = monero!.formatterMoneroParseAmount(amount: _cryptoAmount);
break;
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.bitcoinCash:
case WalletType.dogecoin:
_amount = cryptoCurrencyHandler().parseAmount(_cryptoAmount).toInt();
break;
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
_amount = evm!.formatterEVMParseAmount(_cryptoAmount);
break;
case WalletType.wownero:
_amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount);
break;
case WalletType.none:
case WalletType.nano:
case WalletType.banano:
break;
}
if (_amount > 0) {
amount = _amount;
}
}
return cryptoCurrencyHandler().parseAmount(cryptoAmount.sanitized());
} catch (e) {
amount = 0;
return Money.zero(cryptoCurrencyHandler());
}
return amount;
}
@observable
String estimatedFee;
Money estimatedFee;
@action
Future<void> calculateEstimatedFee() async {
@@ -169,7 +136,7 @@ abstract class OutputBase with Store {
if (_settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId) != null) {
fee = _wallet.calculateEstimatedFee(
_settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId)!,
formattedCryptoAmount,
cryptoAmountMoney.amount.toInt(),
);
}
@@ -179,22 +146,20 @@ abstract class OutputBase with Store {
case WalletType.litecoin:
case WalletType.bitcoinCash:
case WalletType.dogecoin:
estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee));
estimatedFee = Money.fromInt(fee, walletTypeToCryptoCurrency(_wallet.type));
break;
case WalletType.bitcoin:
if (cryptoCurrencyHandler() == CryptoCurrency.btcln) {
estimatedFee =
_appStore.amountParsingProxy.getDisplayCryptoString(10, cryptoCurrencyHandler());
estimatedFee = Money.fromInt(10, cryptoCurrencyHandler());
break;
}
if (_settingsStore.getPriority(_wallet.type) ==
bitcoin!.getBitcoinTransactionPriorityCustom()) {
fee = bitcoin!.getEstimatedFeeWithFeeRate(
_wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount);
_wallet, _settingsStore.customBitcoinFeeRate, cryptoAmountMoney.amount.toInt());
}
estimatedFee =
_appStore.amountParsingProxy.getDisplayCryptoString(fee, cryptoCurrencyHandler());
estimatedFee = Money.fromInt(fee, cryptoCurrencyHandler());
break;
/// EVMs
@@ -203,16 +168,20 @@ abstract class OutputBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
final isNative = cryptoCurrencyHandler() == CryptoCurrency.eth ||
cryptoCurrencyHandler() == CryptoCurrency.maticpoly ||
cryptoCurrencyHandler() == CryptoCurrency.baseEth ||
cryptoCurrencyHandler() == CryptoCurrency.arbEth ||
cryptoCurrencyHandler() == CryptoCurrency.bnb;
String? fee = isNative
final isNative = [
CryptoCurrency.eth,
CryptoCurrency.maticpoly,
CryptoCurrency.baseEth,
CryptoCurrency.arbEth,
CryptoCurrency.bnb,
].contains(cryptoCurrencyHandler());
final fee = isNative
? evm!.getEVMNativeEstimatedFee(_wallet)
: evm!.getEVMERC20EstimatedFee(_wallet);
estimatedFee = formatFixed(BigInt.parse(fee ?? '0.0'), 18, fractionalDigits: 12);
estimatedFee =
Money(BigInt.parse(fee ?? '0.0'), walletTypeToCryptoCurrency(_wallet.type));
break;
/// end EVMs
@@ -235,10 +204,11 @@ abstract class OutputBase with Store {
try {
final currency =
isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler();
isEVMCompatibleChain(_wallet.type) || cryptoCurrencyHandler() == CryptoCurrency.btcln
? _wallet.currency
: cryptoCurrencyHandler();
final cryptoAmount = double.parse(
_appStore.amountParsingProxy.getCanonicalCryptoAmount(estimatedFee, currency));
final cryptoAmount = double.parse(estimatedFee.toString());
return calculateFiatAmountRaw(
price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount);
@@ -252,7 +222,7 @@ abstract class OutputBase with Store {
WalletType get walletType => _wallet.type;
final CryptoCurrency Function() cryptoCurrencyHandler;
final CryptoCurrency Function([CryptoCurrency?]) cryptoCurrencyHandler;
final FiatConversionStore _fiatConversationStore;
final AppStore _appStore;
@@ -270,6 +240,7 @@ abstract class OutputBase with Store {
void updateWallet(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> newWallet) {
_wallet = newWallet;
estimatedFee = Money.zero(cryptoCurrencyHandler());
}
@action
@@ -283,10 +254,22 @@ abstract class OutputBase with Store {
resetParsedAddress();
}
@action
void resetParsedAddress() {
displayName = null;
extractedAddress = '';
parsedAddress = ParsedAddress(addresses: []);
note = '';
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {});
}
@action
void applyAddressLookupResult(ParsedAddress result) {
final currency = cryptoCurrencyHandler();
parsedAddress = result;
extractedAddress = result.parsedAddressByCurrencyMap[currency] ?? '';
note = result.description;
displayName = result.profileName.isNotEmpty ? result.profileName : result.handle;
}
@action
@@ -332,10 +315,21 @@ abstract class OutputBase with Store {
? CryptoCurrency.btc
: cryptoCurrencyHandler();
final price = _fiatConversationStore.prices[cryptoCurrency];
if (price == null || !price.isFinite || price <= 0) {
printV('_updateCryptoAmount: invalid fiat rate $price for $cryptoCurrency');
cryptoAmount = '';
return;
}
final decimals = min(20, cryptoCurrencyHandler().decimals);
final crypto = (double.parse(fiatAmount.replaceAll(',', '.')) /
_fiatConversationStore.prices[cryptoCurrency]!)
.toStringAsFixed(decimals);
final rawCrypto = double.parse(fiatAmount.replaceAll(',', '.')) / price;
if (!rawCrypto.isFinite) {
cryptoAmount = '';
return;
}
final crypto = rawCrypto.toStringAsFixed(decimals);
if (cryptoAmount != crypto) cryptoAmount = crypto;
} catch (e) {
@@ -346,29 +340,26 @@ abstract class OutputBase with Store {
Map<String, dynamic> get extra {
final fields = <String, dynamic>{};
if (parsedAddress.parseFrom == ParseFrom.bip353) {
fields['bip353_name'] = parsedAddress.name;
if (parsedAddress.addressSource == AddressSource.bip353) {
fields['bip353_name'] = parsedAddress.handle;
fields['bip353_proof'] = parsedAddress.bip353DnsProof;
}
return fields;
}
Future<void> fetchParsedAddress(BuildContext context) async {
final domain = address;
final currency = cryptoCurrencyHandler();
parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
if (parsedAddress.name.isNotEmpty) {
displayName = parsedAddress.name;
}
extractedAddress = await extractAddressFromParsed(context, parsedAddress);
note = parsedAddress.description;
}
@action
void loadContact(ContactBase contact) {
address = contact.name;
parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name);
extractedAddress = parsedAddress.addresses.first;
note = parsedAddress.description;
final currency = cryptoCurrencyHandler();
address = contact.address;
applyAddressLookupResult(
ParsedAddress(
parsedAddressByCurrencyMap: {currency: contact.address},
addressSource: AddressSource.contact,
handle: contact.name,
profileName: contact.name,
),
);
}
}
@@ -1,104 +0,0 @@
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/view_model/send/template_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:hash_wallet/entities/template.dart';
import 'package:hash_wallet/store/templates/send_template_store.dart';
import 'package:hash_wallet/core/template_validator.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_wallet/core/amount_validator.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
part 'send_template_view_model.g.dart';
class SendTemplateViewModel = SendTemplateViewModelBase with _$SendTemplateViewModel;
abstract class SendTemplateViewModelBase with Store {
final WalletBase _wallet;
final AppStore _appStore;
final SendTemplateStore _sendTemplateStore;
final FiatConversionStore _fiatConversationStore;
SendTemplateViewModelBase(
this._wallet, this._appStore, this._sendTemplateStore, this._fiatConversationStore)
: recipients = ObservableList<TemplateViewModel>() {
addRecipient();
}
ObservableList<TemplateViewModel> recipients;
@action
void addRecipient() {
recipients.add(TemplateViewModel(
wallet: _wallet, appStore: _appStore, fiatConversationStore: _fiatConversationStore));
}
@action
void removeRecipient(TemplateViewModel recipient) {
recipients.remove(recipient);
}
AmountValidator get amountValidator => AmountValidator(
currency: walletTypeToCryptoCurrency(_wallet.type, chainId: _wallet.chainId),
amountParsingProxy: _appStore.amountParsingProxy,
);
AddressValidator get addressValidator =>
AddressValidator(type: _wallet.currency, isTestnet: _wallet.isTestnet);
TemplateValidator get templateValidator => TemplateValidator();
bool get hasMultiRecipient => !isEVMCompatibleChain(_wallet.type);
@computed
CryptoCurrency get cryptoCurrency => _wallet.currency;
@computed
String get fiatCurrency => _appStore.settingsStore.fiatCurrency.title;
@computed
int get fiatCurrencyDecimals => _appStore.settingsStore.fiatCurrency.decimals;
@computed
ObservableList<Template> get templates => _sendTemplateStore.templates;
@action
void updateTemplate() => _sendTemplateStore.update();
@action
void addTemplate(
{required String name,
required bool isCurrencySelected,
required String cryptoCurrency,
required String address,
required String amount,
required String amountFiat,
required List<Template> additionalRecipients}) {
_sendTemplateStore.addTemplate(
name: name,
isCurrencySelected: isCurrencySelected,
address: address,
cryptoCurrency: cryptoCurrency,
fiatCurrency: fiatCurrency,
amount: amount,
amountFiat: amountFiat,
additionalRecipients: additionalRecipients);
updateTemplate();
}
@action
void removeTemplate({required Template template}) {
_sendTemplateStore.remove(template: template);
updateTemplate();
}
@computed
List<CryptoCurrency> get walletCurrencies => _wallet.balance.keys.toList();
bool get hasMultipleTokens => isEVMCompatibleChain(_wallet.type);
}
+192 -57
View File
@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_wallet/core/amount_parsing_proxy.dart';
import 'package:hash_wallet/core/amount_validator.dart';
@@ -8,6 +10,7 @@ import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/core/open_crypto_pay/exceptions.dart';
import 'package:hash_wallet/core/open_crypto_pay/models.dart';
import 'package:hash_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
import 'package:hash_wallet/core/utilities.dart';
import 'package:hash_wallet/core/validator.dart';
import 'package:hash_wallet/core/wallet_change_listener_view_model.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
@@ -15,7 +18,6 @@ import 'package:hash_wallet/entities/contact.dart';
import 'package:hash_wallet/entities/contact_record.dart';
import 'package:hash_wallet/entities/evm_transaction_error_fees_handler.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/entities/parsed_address.dart';
import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/entities/template.dart';
import 'package:hash_wallet/entities/transaction_description.dart';
@@ -38,16 +40,18 @@ import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/view_model/send/fees_view_model.dart';
import 'package:hash_wallet/view_model/send/output.dart';
import 'package:hash_wallet/view_model/send/send_template_view_model.dart';
import 'package:hash_wallet/view_model/send/send_view_model_state.dart';
import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/lnurl.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -85,8 +89,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
SendViewModelBase(
this._appStore,
this.sendTemplateViewModel,
this._fiatConversationStore,
this._adrResService,
this.balanceViewModel,
this.contactListViewModel,
this.transactionDescriptionBox,
@@ -105,11 +109,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
fiatFromSettings = _appStore.settingsStore.fiatCurrency,
fiatCurrencies = FiatCurrency.all,
super(appStore: _appStore) {
outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency));
outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler));
unspentCoinsListViewModel
.initialSetup()
.then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections());
unspentCoinsListViewModel.initialSetup();
// .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections());
reaction((_) {
if (isEVMCompatibleChain(wallet.type)) {
@@ -119,9 +122,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return null;
}, (_) async {
// When chain changes, update currencies and selected currency
final selectionAtChainChange = selectedCryptoCurrency;
await Future.delayed(const Duration(milliseconds: 100));
currencies = wallet.balance.keys.toList();
selectedCryptoCurrency = wallet.currency;
if (selectedCryptoCurrency == selectionAtChainChange) {
selectedCryptoCurrency = wallet.currency;
}
updateSendingBalance();
});
}
@@ -142,18 +148,48 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool get isEVMWallet => isEVMCompatibleChain(walletType);
CryptoCurrency _outputCryptoCurrencyHandler([CryptoCurrency? override]) {
if (override != null && override != selectedCryptoCurrency) {
runInAction(() => selectedCryptoCurrency = override);
}
return selectedCryptoCurrency;
}
@action
void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value;
@action
void addOutput() =>
outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency));
outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler));
@action
void removeOutput(Output output) {
if (isBatchSending) outputs.remove(output);
}
Future<ParsedAddress?> resolveAddressForOutput(Output output) async {
final query = output.address.trim();
if (query.isEmpty) return null;
// Check if the address is valid for the current currency (except for Zano, which can use handles as addresses)
if (selectedCryptoCurrency != CryptoCurrency.zano) {
final isValidAddress = AddressValidator(
type: selectedCryptoCurrency,
network: wallet.walletInfo.network,
).isValid(query);
if (isValidAddress) return null;
}
final results = await _adrResService.resolve(
query: query,
wallet: wallet,
currency: selectedCryptoCurrency,
);
return results.isEmpty ? null : results.first;
}
@action
void clearOutputs() {
outputs.clear();
@@ -264,8 +300,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Validator<String> get allAmountValidator => AllAmountValidator();
Validator<String> get addressValidator =>
AddressValidator(type: selectedCryptoCurrency, isTestnet: wallet.isTestnet);
Validator<String> get addressValidator => AddressValidator(
type: selectedCryptoCurrency,
isTestnet: wallet.isTestnet,
network: wallet.walletInfo.network,
);
Validator<String> get textValidator => TextValidator();
@@ -274,6 +313,24 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@observable
PendingTransaction? pendingTransaction;
// Bumped when a preparation is dismissed or replaced.
//
// Finished call sets pendingTransaction only if it matches this value to
// its own value.
int _sendPreparationId = 0;
String? get pendingTransactionAdditionalCostNotice {
final additionalCost = pendingTransaction?.additionalCost;
if (additionalCost == null) {
return null;
}
return S.current.recipient_account_creation_fee(
_appStore.amountParsingProxy.asDisplayStringWithSymbol(additionalCost),
);
}
@computed
String get balance {
if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.mweb) {
@@ -286,10 +343,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// Handle case where balance might not be available yet (e.g., during chain switch)
final balanceForCurrency = wallet.balance[selectedCryptoCurrency];
if (balanceForCurrency == null) {
return _appStore.amountParsingProxy.getDisplayCryptoString(0, selectedCryptoCurrency);
return _appStore.amountParsingProxy.asDisplayString(Money.zero(selectedCryptoCurrency));
}
return _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(
wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance, selectedCryptoCurrency);
return _appStore.amountParsingProxy
.asDisplayString(wallet.balance[selectedCryptoCurrency]!.available);
}
@action
@@ -316,7 +373,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// only for electrum, monero, wownero wallets atm:
switch (wallet.type) {
case WalletType.bitcoin:
if (coinTypeToSpendFrom == UnspentCoinType.lightning) return balance;
if (selectedCryptoCurrency == CryptoCurrency.btcln) return balance;
return _appStore.amountParsingProxy.getDisplayCryptoString(
await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom),
walletTypeToCryptoCurrency(walletType));
@@ -350,31 +407,20 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// If silent payments scanning, can still send payments
(wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus);
bool isSendToSilentPayments(Output output) =>
wallet.type == WalletType.bitcoin &&
(RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(output.address) ||
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
.hasMatch(output.extractedAddress) ||
(output.parsedAddress.addresses.isNotEmpty &&
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
.hasMatch(output.parsedAddress.addresses[0])));
bool isSendToSilentPayments(Output output) {
if (wallet.type != WalletType.bitcoin) return false;
final sp = RegExp(AddressValidator.silentPaymentAddressPatternMainnet);
final address = output.extractedAddress.isNotEmpty ? output.extractedAddress : output.address;
return sp.hasMatch(address);
}
@computed
List<Template> get templates => sendTemplateViewModel.templates
.where((template) => _isEqualCurrency(template.cryptoCurrency))
.toList();
@computed
bool get hasCoinControl =>
[
WalletType.bitcoin,
WalletType.litecoin,
WalletType.monero,
WalletType.wownero,
WalletType.bitcoinCash,
WalletType.dogecoin
].contains(wallet.type) &&
coinTypeToSpendFrom != UnspentCoinType.lightning;
wallet.hasCoinControl && coinTypeToSpendFrom != UnspentCoinType.lightning;
@computed
bool get hasFees => feesViewModel.hasFees && coinTypeToSpendFrom != UnspentCoinType.lightning;
@@ -394,6 +440,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
String get selectedCryptoCurrencySymbol =>
amountParsingProxy.getCryptoSymbol(selectedCryptoCurrency);
@computed
bool get useBaseUnits => amountParsingProxy.useSatoshi(selectedCryptoCurrency);
List<CryptoCurrency> currencies;
WalletType get walletType => wallet.type;
@@ -411,20 +460,20 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final AppStore _appStore;
SettingsStore get _settingsStore => _appStore.settingsStore;
final SendTemplateViewModel sendTemplateViewModel;
final BalanceViewModel balanceViewModel;
final ContactListViewModel contactListViewModel;
final HardwareWalletViewModel? hardwareWalletViewModel;
final FeesViewModel feesViewModel;
final FiatConversionStore _fiatConversationStore;
final AddressResolverService _adrResService;
final Box<TransactionDescription> transactionDescriptionBox;
@computed
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
@computed
bool get hasMultiRecipient =>
sendTemplateViewModel.hasMultiRecipient && coinTypeToSpendFrom != UnspentCoinType.lightning;
!isEVMCompatibleChain(wallet.type) &&
coinTypeToSpendFrom != UnspentCoinType.lightning;
@computed
String get languageCode => _appStore.settingsStore.languageCode;
@@ -464,6 +513,19 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool get shouldDisplayTOTP2FAForSendsToInternalWallet =>
_settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
@computed
TransactionInfo? get transactionInfo {
if (isEVMCompatibleChain(walletType)) {
return wallet.transactionHistory.transactions[pendingTransaction?.evmTxHashFromRawHex];
}
if (walletType == WalletType.monero) {
// monero tx history keys are in format txhash_amount_accountindex_addressindex
return wallet.transactionHistory.transactions[wallet.transactionHistory.transactions.keys
.firstWhereOrNull((item) => item.split("_").first == pendingTransaction?.id)];
}
return wallet.transactionHistory.transactions[pendingTransaction?.id];
}
//* Still open to further optimize these checks
//* It works but can be made better
@action
@@ -498,6 +560,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@action
Future<void> dismissTransaction() async {
_sendPreparationId++;
_ledgerTxStateTimer?.cancel();
_ledgerTxStateTimer = null;
state = InitialExecutionState();
if (ocpRequest != null) {
clearOutputs();
@@ -550,8 +615,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
clearOutputs();
outputs.first.address = paymentRequest.address;
outputs.first.parsedAddress =
ParsedAddress(addresses: [paymentRequest.address], name: ocpRequest!.receiverName);
outputs.first.parsedAddress = ParsedAddress(
parsedAddressByCurrencyMap: {currency: paymentRequest.address},
handle: ocpRequest!.receiverName);
outputs.first.setCryptoAmount(paymentRequest.amount);
outputs.first.note = ocpRequest!.receiverName;
@@ -587,20 +653,38 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@action
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider, Trade? trade}) async {
final preparationId = ++_sendPreparationId;
_ledgerTxStateTimer?.cancel();
_ledgerTxStateTimer = null;
pendingTransaction = null;
Timer? preparationTimer;
bool replaced() {
if (preparationId == _sendPreparationId) {
return false;
}
preparationTimer?.cancel();
return true;
}
try {
if (!(state is IsExecutingState)) state = IsExecutingState();
if (wallet.isHardwareWallet) {
state = IsAwaitingDeviceResponseState();
if (walletType == WalletType.monero) {
_ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
preparationTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (preparationId != _sendPreparationId) {
timer.cancel();
return;
}
if (monero!.getLastLedgerCommand() == "INS_CLSAG") {
timer.cancel();
state = IsDeviceSigningResponseState();
}
});
_ledgerTxStateTimer = preparationTimer;
} else {
state = IsAwaitingDeviceResponseState();
}
}
@@ -609,19 +693,32 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final isSendAll = outputs.any((output) => output.sendAll);
if (!isSendAll) {
final estimateTxAmountDouble = outputs.fold<double>(
0, (acc, output) => acc + (double.tryParse(output.cryptoAmount) ?? 0));
if (estimateTxAmountDouble <= 0) throw Exception('Amount must be greater than 0');
final estimateTxAmount = outputs.fold<BigInt>(
BigInt.zero, (acc, output) => acc + output.cryptoAmountMoney.amount);
if (estimateTxAmount <= BigInt.zero) {
throw Exception('Amount must be greater than 0');
}
}
pendingTransaction = await wallet.createTransaction(_credentials(provider));
final prepared = await wallet.createTransaction(_credentials(provider));
if (replaced()) {
return null;
}
pendingTransaction = prepared;
final txAmountDouble = double.tryParse(pendingTransaction?.amountFormatted ?? '0') ?? 0.0;
final bool isTradeTx = trade != null && provider != null;
if (isTradeTx) {
final tradeAmountDouble = double.tryParse(trade.amount) ?? 0.0;
if (tradeAmountDouble <= 0) throw Exception('Trade amount must be greater than 0');
final tradeAmountMoney = Money.tryParse(
trade.amount.sanitized(),
trade.from ?? selectedCryptoCurrency,
strictParsing: false,
);
if (tradeAmountMoney == null || tradeAmountMoney.sign <= 0) {
throw Exception('Trade amount must be greater than 0');
}
final tradeAmountDouble = double.tryParse(tradeAmountMoney.toString()) ?? 0.0;
if (trade.isSendAll == true) {
if (provider is NearIntentsExchangeProvider) {
@@ -644,6 +741,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
state = ExecutedSuccessfullyState();
return pendingTransaction;
} catch (e) {
if (replaced()) {
return null;
}
_ledgerTxStateTimer?.cancel();
// if (e is LedgerException) {
// final errorCode = e.errorCode.toRadixString(16);
@@ -721,7 +821,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
try {
state = TransactionCommitting();
state = wallet.isHardwareWallet && walletType == WalletType.monero
? IsAwaitingDeviceResponseState()
: TransactionCommitting();
if (ocpRequest != null) {
await _handleOcpRequest();
@@ -747,6 +849,28 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
});
}
// FIXME(malik) ideally, this should be done wallet-side.
// it is required because evm, solana and tron don't actually save the transaction info when you send something.
// instead, they rely on the tx to eventually get fetched at sync time, which can take a while
if (isEVMWallet) {
final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull(
(token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(),
);
wallet.transactionHistory.addOne(evm!.getTransactionInfo(
id: pendingTransaction!.evmTxHashFromRawHex!,
height: 0,
amount: outputs.first.cryptoAmountMoney,
fee: pendingTransaction!.fee,
tokenSymbol: selectedCryptoCurrency.title,
direction: TransactionDirection.outgoing,
isPending: true,
date: DateTime.now(),
confirmations: 0,
chainId: wallet.chainId ?? 0,
contractAddress: selectedToken?.contractAddress,
));
}
if (pendingTransaction!.id.isNotEmpty) {
_addTransactionDescription();
}
@@ -763,9 +887,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Future<void> _addTransactionDescription() async {
String address = outputs.fold('', (acc, value) {
return value.isParsedAddress
? '$acc${value.address}\n${value.extractedAddress}\n\n'
: '$acc${value.address}\n\n';
final canonical = value.extractedAddress.trim().isNotEmpty
? value.extractedAddress.trim()
: value.address.trim();
if (canonical.isEmpty) {
return acc;
}
return '$acc$canonical\n\n';
});
address = address.trim();
@@ -891,8 +1019,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
for (final output in outputs) {
final address =
output.isParsedAddress ? output.parsedAddress.addresses.first : output.address;
final address = output.isParsedAddress
? output.parsedAddress.parsedAddressByCurrencyMap[selectedCryptoCurrency] ?? ''
: output.address;
if (address.isNotEmpty &&
!contactAddresses.contains(address) &&
@@ -1035,9 +1164,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
String? payjoinUri;
@action
Future<void> fetchTokenForContractAddress(String contractAddress) async {
Future<void> fetchTokenForContractAddress(String contractAddress,
{WalletType? walletType}) async {
final token = await TokenUtilities.findTokenByAddress(
walletType: wallet.type,
walletType: walletType ?? wallet.type,
address: contractAddress,
);
@@ -1045,4 +1175,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
selectedCryptoCurrency = token;
}
}
@action
void applyAnyPayCurrency(CryptoCurrency currency) {
selectedCryptoCurrency = currency;
}
}
@@ -1,75 +0,0 @@
import 'package:hash_wallet/entities/template.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/view_model/send/output.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
part 'template_view_model.g.dart';
class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel;
abstract class TemplateViewModelBase with Store {
final WalletBase _wallet;
final AppStore _appStore;
final FiatConversionStore _fiatConversationStore;
TemplateViewModelBase({
required WalletBase wallet,
required AppStore appStore,
required FiatConversionStore fiatConversationStore,
}) : _wallet = wallet,
_appStore = appStore,
_fiatConversationStore = fiatConversationStore,
_currency = wallet.currency,
output = Output(wallet, appStore, fiatConversationStore, () => wallet.currency) {
output = Output(_wallet, _appStore, _fiatConversationStore, () => _currency);
}
@observable
Output output;
@observable
String name = '';
@observable
String address = '';
@observable
CryptoCurrency _currency;
@observable
bool isCryptoSelected = true;
@action
void setCryptoCurrency(bool value) => isCryptoSelected = value;
@action
void reset() {
name = '';
address = '';
isCryptoSelected = true;
output.reset();
}
Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) {
return Template(
isCurrencySelectedRaw: isCryptoSelected,
nameRaw: name,
addressRaw: address,
cryptoCurrencyRaw: cryptoCurrency,
fiatCurrencyRaw: fiatCurrency,
amountRaw: output.cryptoAmount,
amountFiatRaw: output.fiatAmount);
}
@action
void changeSelectedCurrency(CryptoCurrency currency) {
isCryptoSelected = true;
_currency = currency;
}
@computed
CryptoCurrency get selectedCurrency => _currency;
}
@@ -1,12 +1,13 @@
import 'dart:async';
import 'package:hash_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/entities/exchange_api_mode.dart';
import 'package:hash_wallet/entities/fiat_api_mode.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/utils/show_pop_up.dart';
import 'package:hash_wallet/utils/tor.dart';
@@ -49,9 +50,30 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get looksUpENS => _settingsStore.lookupsENS;
@computed
bool get lookupsZcashNames => _settingsStore.lookupsZcashNames;
@computed
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
@computed
bool get lookupsNostr => _settingsStore.lookupsNostr;
@computed
bool get lookupsFio => _settingsStore.lookupsFio;
@computed
bool get lookupsBip353 => _settingsStore.lookupsBip353;
@computed
bool get lookupsThorChain => _settingsStore.lookupsThorChain;
@computed
bool get lookupsZcashAddress => _settingsStore.lookupsZcashAddress;
@computed
bool get lookupsLNUrlPay => _settingsStore.lookupsLNUrl;
@computed
ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus;
@@ -65,6 +87,100 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get builtinTor => _settingsStore.currentBuiltinTor;
List<AddressSource> get domainLookupSources => AddressResolverService.supportedSources;
bool lookupValue(AddressSource source) {
switch (source) {
case AddressSource.twitter:
return lookupTwitter;
case AddressSource.mastodon:
return looksUpMastodon;
case AddressSource.yatRecord:
return looksUpYatService;
case AddressSource.unstoppableDomains:
return looksUpUnstoppableDomains;
case AddressSource.openAlias:
return looksUpOpenAlias;
case AddressSource.ens:
return looksUpENS;
case AddressSource.zcashName:
return lookupsZcashNames;
case AddressSource.zcashAddress:
return lookupsZcashAddress;
case AddressSource.wellKnown:
return looksUpWellKnown;
case AddressSource.zanoAlias:
return lookupsZanoAlias;
case AddressSource.bip353:
return lookupsBip353;
case AddressSource.fio:
return lookupsFio;
case AddressSource.lnurlPay:
return lookupsLNUrlPay;
case AddressSource.thorChain:
return lookupsThorChain;
case AddressSource.nostr:
return lookupsNostr;
case AddressSource.contact:
case AddressSource.notParsed:
return false;
}
}
@action
void setLookupValue(AddressSource source, bool value) {
switch (source) {
case AddressSource.twitter:
_settingsStore.lookupsTwitter = value;
break;
case AddressSource.mastodon:
_settingsStore.lookupsMastodon = value;
break;
case AddressSource.yatRecord:
_settingsStore.lookupsYatService = value;
break;
case AddressSource.unstoppableDomains:
_settingsStore.lookupsUnstoppableDomains = value;
break;
case AddressSource.openAlias:
_settingsStore.lookupsOpenAlias = value;
break;
case AddressSource.ens:
_settingsStore.lookupsENS = value;
break;
case AddressSource.zcashName:
_settingsStore.lookupsZcashNames = value;
break;
case AddressSource.zcashAddress:
_settingsStore.lookupsZcashAddress = value;
break;
case AddressSource.wellKnown:
_settingsStore.lookupsWellKnown = value;
break;
case AddressSource.zanoAlias:
_settingsStore.lookupsZanoAlias = value;
break;
case AddressSource.bip353:
_settingsStore.lookupsBip353 = value;
break;
case AddressSource.fio:
_settingsStore.lookupsFio = value;
break;
case AddressSource.lnurlPay:
_settingsStore.lookupsLNUrl = value;
break;
case AddressSource.thorChain:
_settingsStore.lookupsThorChain = value;
break;
case AddressSource.nostr:
_settingsStore.lookupsNostr = value;
break;
case AddressSource.contact:
case AddressSource.notParsed:
break;
}
}
@computed
bool get hasPowNodes => [WalletType.nano, WalletType.banano].contains(_wallet.type);
@@ -88,6 +204,9 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get useBaseScan => _settingsStore.useBaseScan;
@computed
bool get useRobinhoodScan => _settingsStore.useRobinhoodScan;
@computed
bool get useArbiScan => _settingsStore.useArbiScan;
@@ -103,36 +222,15 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get canUseBaseScan => _wallet.chainId == 8453;
@computed
bool get canUseRobinhoodScan => _wallet.chainId == 4663;
@computed
bool get canUseArbiScan => _wallet.chainId == 42161;
@computed
bool get canUseBscScan => _wallet.chainId == 56;
@action
void setLookupsTwitter(bool value) => _settingsStore.lookupsTwitter = value;
@action
void setLookupsZanoAlias(bool value) => _settingsStore.lookupsZanoAlias = value;
@action
void setLookupsMastodon(bool value) => _settingsStore.lookupsMastodon = value;
@action
void setLookupsENS(bool value) => _settingsStore.lookupsENS = value;
@action
void setLookupsWellKnown(bool value) => _settingsStore.lookupsWellKnown = value;
@action
void setLookupsYatService(bool value) => _settingsStore.lookupsYatService = value;
@action
void setLookupsUnstoppableDomains(bool value) => _settingsStore.lookupsUnstoppableDomains = value;
@action
void setLookupsOpenAlias(bool value) => _settingsStore.lookupsOpenAlias = value;
@action
void setUseMempoolFeeAPI(bool value) => _settingsStore.useMempoolFeeAPI = value;
@@ -149,6 +247,11 @@ abstract class ConnectionSyncViewModelBase with Store {
@action
void setUseBlinkProtection(bool value) => _settingsStore.useBlinkProtection = value;
bool get hasRescan =>
_wallet.hasRescan &&
_wallet.type != WalletType.bitcoin &&
_wallet.type != WalletType.litecoin;
@action
void setUseEtherscan(bool value) {
_settingsStore.useEtherscan = value;
@@ -167,6 +270,12 @@ abstract class ConnectionSyncViewModelBase with Store {
evm!.updateScanProviderUsageState(_wallet, value);
}
@action
void setUseRobinhoodScan(bool value) {
_settingsStore.useRobinhoodScan = value;
evm!.updateScanProviderUsageState(_wallet, value);
}
@action
void setUseArbiScan(bool value) {
_settingsStore.useArbiScan = value;
@@ -47,6 +47,8 @@ abstract class OtherSettingsViewModelBase with Store {
final SettingsStore _settingsStore;
final SendViewModel sendViewModel;
bool get hasSignVerify => _wallet.canSignMessages;
@computed
TransactionPriority get transactionPriority {
final priority = _settingsStore.getPriority(walletType, chainId: chainId);
@@ -156,6 +158,12 @@ abstract class OtherSettingsViewModelBase with Store {
void setShouldSaveRecipientAddress(bool value) =>
_settingsStore.shouldSaveRecipientAddress = value;
@computed
bool get showCiBuildOverlay => _settingsStore.showCiBuildOverlay;
@action
void setShowCiBuildOverlay(bool value) => _settingsStore.showCiBuildOverlay = value;
int? get customPriorityItemIndex {
final priorities = priorityForWalletType(walletType);
final customItem = priorities
@@ -170,6 +178,10 @@ abstract class OtherSettingsViewModelBase with Store {
return null;
}
bool get hasPayjoin => _wallet.hasPayjoinSupport;
bool get hasLightning => _wallet.hasLightningSupport;
Future<File?> getLightningLog() async {
final path = await pathForWalletDir(name: _wallet.name, type: walletType);
final logFile = File("$path/lightning.log");
@@ -25,7 +25,12 @@ abstract class PrivacySettingsViewModelBase with Store {
bool get isBitcoin => _wallet.type == WalletType.bitcoin;
@computed
bool get hasMWEB => _wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid);
bool get hasSilentPaymentsScanning => _wallet.hasSilentPaymentsScanning;
@computed
bool get hasMWEB =>
_wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) &&
!_wallet.isHardwareWallet;
@computed
bool get isAutoGenerateSubaddressesEnabled =>
@@ -48,14 +53,7 @@ abstract class PrivacySettingsViewModelBase with Store {
].contains(_wallet.type);
@computed
bool get hasCoinControl => [
WalletType.bitcoin,
WalletType.litecoin,
WalletType.monero,
WalletType.wownero,
WalletType.bitcoinCash,
WalletType.dogecoin
].contains(_wallet.type);
bool get hasCoinControl => _wallet.hasCoinControl;
bool get isMoneroWallet => _wallet.type == WalletType.monero;
@@ -96,6 +94,9 @@ abstract class PrivacySettingsViewModelBase with Store {
@computed
bool get looksUpENS => _settingsStore.lookupsENS;
@computed
bool get lookupsZcashNames => _settingsStore.lookupsZcashNames;
@computed
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
@@ -103,11 +104,10 @@ abstract class PrivacySettingsViewModelBase with Store {
bool get usePayjoin => _settingsStore.usePayjoin;
@computed
bool get canUsePayjoin => _wallet.type == WalletType.bitcoin && DeviceInfo.instance.isMobile;
bool get canUsePayjoin => _wallet.hasPayjoinSupport && DeviceInfo.instance.isMobile;
@computed
bool get canUseLightning =>
_wallet.type == WalletType.bitcoin && !Platform.isWindows && !Platform.isLinux;
bool get canUseLightning => _wallet.hasLightningSupport;
@computed
bool get useLightning => _wallet.type == WalletType.bitcoin && bitcoin!.useLightning(_wallet);
@@ -1,8 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hash_wallet/view_model/settings/settings_list_item.dart';
class RegularListItem extends SettingsListItem {
RegularListItem({required String title, this.handler}) : super(title);
final void Function(BuildContext context)? handler;
}
@@ -1,11 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:hash_wallet/view_model/settings/settings_list_item.dart';
class SwitcherListItem extends SettingsListItem {
SwitcherListItem({required String title, required this.value, required this.onValueChange})
: super(title);
final bool Function() value;
final void Function(BuildContext context, bool value) onValueChange;
}
+6 -6
View File
@@ -50,11 +50,11 @@ abstract class SupportViewModelBase with Store {
icon: 'assets/images/change_now.png',
linkTitle: 'support@changenow.io',
link: 'mailto:support@changenow.io'),
LinkListItem(
title: 'SideShift',
icon: 'assets/images/sideshift.png',
linkTitle: 'help.sideshift.ai',
link: 'https://help.sideshift.ai/en/'),
// LinkListItem(
// title: 'SideShift',
// icon: 'assets/images/sideshift.png',
// linkTitle: 'help.sideshift.ai',
// link: 'https://help.sideshift.ai/en/'),
LinkListItem(
title: 'SimpleSwap',
icon: 'assets/images/simpleSwap.png',
@@ -69,7 +69,7 @@ abstract class SupportViewModelBase with Store {
title: 'SwapTrade',
icon: 'assets/images/swap_trade.png',
linkTitle: 'help.swaptrade.io',
link: 'mailto:support@exolix.com'),
link: 'https://help.swaptrade.io/contact/'),
LinkListItem(
title: 'Trocador',
icon: 'assets/images/trocador.png',
@@ -14,6 +14,7 @@ import 'package:hash_wallet/src/screens/transaction_details/standart_list_item.d
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/utils/date_formatter.dart';
import 'package:hash_wallet/utils/show_bar.dart';
import 'package:cw_core/currencies_with_memo.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
@@ -135,6 +136,16 @@ abstract class TradeDetailsViewModelBase with Store {
));
}
final destinationMemo = trade.toAddressExtraId;
final destinationCurrency = trade.to;
if (destinationMemo != null && destinationMemo.isNotEmpty && destinationCurrency != null) {
final isDestinationTag =
memoLabelTypeFor(destinationCurrency) == MemoLabelType.destinationTag;
items.add(StandartListItem(
title: isDestinationTag ? S.current.destination_tag : S.current.memo,
value: destinationMemo));
}
items.add(StandartListItem(
title: S.current.trade_details_provider, value: trade.provider.toString()));
+270 -220
View File
@@ -1,45 +1,65 @@
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/screens/transaction_details/address_list_item.dart';
import 'package:hash_wallet/src/screens/transaction_details/confirmations_list_item.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/priority_for_wallet_type.dart';
import 'package:hash_wallet/entities/transaction_description.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart';
import 'package:hash_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:hash_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
import 'package:hash_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart';
import 'package:hash_wallet/utils/date_formatter.dart';
import 'package:hash_wallet/view_model/send/send_view_model.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/core/address_validator.dart";
import "package:hash_wallet/entities/priority_for_wallet_type.dart";
import "package:hash_wallet/entities/transaction_description.dart";
import "package:hash_wallet/evm/evm.dart";
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/monero/monero.dart";
import "package:hash_wallet/reactions/wallet_connect.dart";
import "package:hash_wallet/src/screens/transaction_details/address_list_item.dart";
import "package:hash_wallet/src/screens/transaction_details/confirmations_list_item.dart";
import "package:hash_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart";
import "package:hash_wallet/src/screens/transaction_details/standart_list_item.dart";
import "package:hash_wallet/src/screens/transaction_details/transaction_details_list_item.dart";
import "package:hash_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:hash_wallet/view_model/send/send_view_model.dart";
import "package:collection/collection.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/transaction_direction.dart";
import "package:cw_core/transaction_info.dart";
import "package:cw_core/transaction_priority.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/foundation.dart";
import "package:hive/hive.dart";
import "package:intl/intl.dart";
import "package:mobx/mobx.dart";
import "package:url_launcher/url_launcher.dart";
part 'transaction_details_view_model.g.dart';
part "transaction_details_view_model.g.dart";
bool _trueFunc(_) => true;
bool isLightning(TransactionInfo tx) {
printV(tx.additionalInfo);
return (tx.additionalInfo["isLightning"] as bool?) ?? false;
/// We're adding a regex here so we can remove any already saved address that has the account in it.
/// In the refactor, we will make another separate variable for accounts and the UI would handle it as needed.
String _moneroRecipientAddressForDisplay(String raw, WalletType walletType) {
if (walletType != WalletType.monero || raw.isEmpty) {
return raw;
}
final compact = raw.replaceAll(RegExp(r"\s"), "");
final match = RegExp(
r"4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}",
caseSensitive: false,
).firstMatch(compact);
return match?.group(0) ?? raw.trim();
}
bool isLightning(TransactionInfo tx) => (tx.additionalInfo["isLightning"] as bool?) ?? false;
bool hasLightningPreimage(TransactionInfo tx) => (tx.additionalInfo["preimage"] as String?) != null;
class TxDetailRowDefinition {
TxDetailRowDefinition({
required this.keyString,
required this.title,
required this.valueGetter,
this.applicable = _trueFunc,
this.listItemBuilder = StandartListItem.new,
});
final String keyString;
final String title;
final String Function(TransactionDetailsViewModelBase) valueGetter;
@@ -50,107 +70,120 @@ class TxDetailRowDefinition {
required Key key,
}) listItemBuilder;
TxDetailRowDefinition(
{required this.keyString,
required this.title,
required this.valueGetter,
this.applicable = _trueFunc,
this.listItemBuilder = StandartListItem.new});
static final List<TxDetailRowDefinition> defs = [
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_date_key",
title: S.current.transaction_details_date,
valueGetter: (vm) => DateFormatter.withCurrentLocal().format(vm.transactionInfo.date)),
keyString: "standard_list_item_transaction_details_date_key",
title: S.current.transaction_details_date,
valueGetter: (vm) => DateFormat("d MMMM yyyy, HH:mm", vm._appStore.settingsStore.languageCode)
.format(vm.transactionInfo.date),
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_height_key",
title: S.current.transaction_details_height,
valueGetter: (vm) => vm.transactionInfo.height?.toString() ?? "",
applicable: (vm) => !isLightning(vm.transactionInfo)),
keyString: "standard_list_item_transaction_details_height_key",
title: S.current.transaction_details_height,
valueGetter: (vm) => vm.transactionInfo.height?.toString() ?? "",
applicable: (vm) => !isLightning(vm.transactionInfo),
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_fee_key",
title: S.current.transaction_details_fee,
valueGetter: (vm) => vm.transactionInfo.feeFormatted()!,
applicable: (vm) =>
vm.wallet.type != WalletType.nano &&
(vm.transactionInfo.feeFormatted() ?? "").isNotEmpty),
keyString: "standard_list_item_transaction_details_fee_key",
title: S.current.transaction_details_fee,
valueGetter: (vm) => vm.feeAmount,
applicable: (vm) =>
vm.wallet.type != WalletType.nano &&
(vm.transactionInfo.fee?.toStringWithSymbol() ?? "").isNotEmpty,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_confirmations_key",
title: S.current.confirmations,
valueGetter: (vm) => "${vm.transactionInfo.confirmations}/${vm.neededConfirmations}",
applicable: (vm) =>
[...electrumWalletTypes, ...evmWalletTypes, WalletType.monero]
.contains(vm.wallet.type) &&
!isLightning(vm.transactionInfo),
listItemBuilder: ConfirmationsListItem.new),
keyString: "standard_list_item_transaction_confirmations_key",
title: S.current.confirmations,
valueGetter: (vm) => "${vm.transactionInfo.confirmations}/${vm.neededConfirmations}",
applicable: (vm) =>
[...electrumWalletTypes, ...evmWalletTypes, WalletType.monero]
.contains(vm.wallet.type) &&
!isLightning(vm.transactionInfo),
listItemBuilder: ConfirmationsListItem.new,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_recipient_address_key",
title: S.current.transaction_details_recipient_address,
valueGetter: (vm) {
String? ret = null;
keyString: "standard_list_item_transaction_details_recipient_address_key",
title: S.current.transaction_details_recipient_address,
valueGetter: (vm) {
String? ret;
switch (vm.wallet.type) {
case WalletType.monero:
if (vm.transactionInfo.direction == TransactionDirection.incoming) {
ret = monero!.getTransactionAddress(
vm.wallet,
vm.transactionInfo.additionalInfo['accountIndex'] as int,
vm.transactionInfo.additionalInfo['addressIndex'] as int);
}
case WalletType.bitcoin:
ret = (bitcoin!.getTransactionAddresses(vm.wallet, vm.transactionInfo) ?? [])
.firstOrNull ??
"";
default:
break;
}
if (ret == null) {
ret = vm.transactionInfo.to ?? "";
}
vm.isRecipientAddressShown = ret.isNotEmpty;
return ret;
},
applicable: (vm) =>
vm.showRecipientAddress &&
(vm.transactionInfo.to != null ||
[WalletType.monero].contains(vm.wallet.type) ||
vm.wallet.type == WalletType.bitcoin &&
vm.transactionInfo.direction == TransactionDirection.incoming),
listItemBuilder: AddressListItem.new),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_source_address_key",
title: S.current.transaction_details_source_address,
valueGetter: (vm) => vm.transactionInfo.from!,
applicable: (vm) => vm.transactionInfo.from != null,
listItemBuilder: AddressListItem.new),
TxDetailRowDefinition(
keyString: "standard_list_item_address_label_key",
title: S.current.address_label,
valueGetter: (vm) => monero!.getSubaddressLabel(
vm.wallet,
vm.transactionInfo.additionalInfo['accountIndex'] as int,
vm.transactionInfo.additionalInfo['addressIndex'] as int),
applicable: (vm) => vm.wallet.type == WalletType.monero),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_key",
title: S.current.transaction_key,
valueGetter: (vm) {
final descriptionKey =
'${vm.transactionInfo.txHash}_${vm.wallet.walletAddresses.primaryAddress}';
switch (vm.wallet.type) {
case WalletType.monero:
if (vm.transactionInfo.direction == TransactionDirection.incoming) {
ret = monero!.getTransactionAddress(
vm.wallet,
vm.transactionInfo.additionalInfo["accountIndex"] as int,
vm.transactionInfo.additionalInfo["addressIndex"] as int,
);
}
case WalletType.bitcoin:
ret = (bitcoin!.getTransactionAddresses(vm.wallet, vm.transactionInfo) ?? [])
.firstOrNull ??
"";
default:
break;
}
ret ??= vm.transactionInfo.to ?? "";
final description = vm.transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == vm.transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
return vm.transactionInfo.additionalInfo['key'] as String? ??
description.transactionKey ??
"";
},
applicable: (vm) => vm.wallet.type == WalletType.monero),
final resolvedAddress = _moneroRecipientAddressForDisplay(ret, vm.wallet.type);
vm.isRecipientAddressShown = resolvedAddress.isNotEmpty;
return resolvedAddress;
},
applicable: (vm) =>
vm.showRecipientAddress &&
(vm.transactionInfo.to != null ||
[WalletType.monero].contains(vm.wallet.type) ||
vm.wallet.type == WalletType.bitcoin &&
vm.transactionInfo.direction == TransactionDirection.incoming),
listItemBuilder: AddressListItem.new,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_confirmed_key",
title: S.current.confirmed_tx,
valueGetter: (vm) => (vm.transactionInfo.confirmations > 0).toString(),
applicable: (vm) => vm.wallet.type == WalletType.nano),
keyString: "standard_list_item_transaction_details_source_address_key",
title: S.current.transaction_details_source_address,
valueGetter: (vm) => vm.transactionInfo.from!,
applicable: (vm) => vm.transactionInfo.from != null,
listItemBuilder: AddressListItem.new,
),
TxDetailRowDefinition(
keyString: "standard_list_item_address_label_key",
title: S.current.address_label,
valueGetter: (vm) => monero!.getSubaddressLabel(
vm.wallet,
vm.transactionInfo.additionalInfo["accountIndex"] as int,
vm.transactionInfo.additionalInfo["addressIndex"] as int,
),
applicable: (vm) => vm.wallet.type == WalletType.monero,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_key",
title: S.current.transaction_key,
valueGetter: (vm) {
final descriptionKey =
"${vm.transactionInfo.txHash}_${vm.wallet.walletAddresses.primaryAddress}";
final description = vm.transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == vm.transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey),
);
return vm.transactionInfo.additionalInfo["key"] as String? ??
description.transactionKey ??
"";
},
applicable: (vm) => vm.wallet.type == WalletType.monero,
),
TxDetailRowDefinition(
keyString: "standard_list_item_lightning_preimage",
title: S.current.transaction_preimage,
valueGetter: (vm) => vm.transactionInfo.additionalInfo["preimage"] as String? ?? "",
applicable: (vm) =>
hasLightningPreimage(vm.transactionInfo) && isLightning(vm.transactionInfo),
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_confirmed_key",
title: S.current.confirmed_tx,
valueGetter: (vm) => (vm.transactionInfo.confirmations > 0).toString(),
applicable: (vm) => vm.wallet.type == WalletType.nano,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_id_key",
title: S.current.transaction_details_transaction_id,
@@ -171,7 +204,7 @@ abstract class TransactionDetailsViewModelBase with Store {
required this.sendViewModel,
this.canReplaceByFee = false,
}) : items = [],
RBFListItems = [],
rbfListItems = [],
newFee = 0,
isRecipientAddressShown = false,
_appStore = appStore,
@@ -180,29 +213,35 @@ abstract class TransactionDetailsViewModelBase with Store {
for (final def in TxDetailRowDefinition.defs) {
if (def.applicable(this)) {
items.add(def.listItemBuilder(
items.add(
def.listItemBuilder(
title: def.title,
value: def.valueGetter(this),
key: ValueKey(def.keyString)) as TransactionDetailsListItem);
key: ValueKey(def.keyString),
) as TransactionDetailsListItem,
);
}
}
_checkForRBF(tx);
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
final description = transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey),
);
if (showRecipientAddress && !isRecipientAddressShown) {
final recipientAddress = description.recipientAddress;
if (recipientAddress?.isNotEmpty ?? false) {
final recipientAddressForDisplay =
_moneroRecipientAddressForDisplay(recipientAddress!, wallet.type);
items.add(
AddressListItem(
title: S.current.transaction_details_recipient_address,
value: recipientAddress!,
key: ValueKey('standard_list_item_${recipientAddress}_key'),
value: recipientAddressForDisplay,
key: ValueKey("standard_list_item_${recipientAddressForDisplay}_key"),
),
);
}
@@ -210,10 +249,11 @@ abstract class TransactionDetailsViewModelBase with Store {
}
void updateNote(String note) {
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
final description = transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey),
);
description.transactionNote = note;
@@ -225,9 +265,10 @@ abstract class TransactionDetailsViewModelBase with Store {
}
String get note {
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
final description = transactionDescriptionBox.values
.firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash);
.firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
);
return description?.transactionNote ?? "";
}
@@ -238,7 +279,7 @@ abstract class TransactionDetailsViewModelBase with Store {
final AppStore _appStore;
final List<TransactionDetailsListItem> items;
final List<TransactionDetailsListItem> RBFListItems;
final List<TransactionDetailsListItem> rbfListItems;
bool showRecipientAddress;
bool isRecipientAddressShown;
int newFee;
@@ -250,27 +291,43 @@ abstract class TransactionDetailsViewModelBase with Store {
return evm!.assetOfTransaction(wallet, transactionInfo);
}
if (isLightning(transactionInfo)) {
return CryptoCurrency.btcln;
}
return walletTypeToCryptoCurrency(wallet.type);
}
// TODO integrate these getters with the TransactionInfo object
@computed
String get transactionAmount =>
_appStore.amountParsingProxy.asDisplayStringWithSymbol(transactionInfo.amount);
@computed
String get feeAmount =>
_appStore.amountParsingProxy.asDisplayStringWithSymbol(transactionInfo.fee!);
@computed
String get transactionCopyAmount =>
_appStore.amountParsingProxy.asDisplayString(transactionInfo.amount);
// TODO(malik1004x): integrate these getters with the TransactionInfo object
String get formattedPendingStatus {
switch (wallet.type) {
case WalletType.monero:
if (transactionInfo.confirmations >= 0 && transactionInfo.confirmations < 10) {
return ' (${transactionInfo.confirmations}/10)';
return " (${transactionInfo.confirmations}/10)";
}
break;
case WalletType.wownero:
if (transactionInfo.confirmations >= 0 && transactionInfo.confirmations < 3) {
return ' (${transactionInfo.confirmations}/3)';
return " (${transactionInfo.confirmations}/3)";
}
break;
case WalletType.litecoin:
bool isPegIn = (transactionInfo.additionalInfo["isPegIn"] as bool?) ?? false;
bool isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
bool fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
String str = '';
final isPegIn = (transactionInfo.additionalInfo["isPegIn"] as bool?) ?? false;
final isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
final fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
String str = "";
if (transactionInfo.confirmations <= 0) {
str = S.current.pending;
}
@@ -287,10 +344,10 @@ abstract class TransactionDetailsViewModelBase with Store {
}
return str;
default:
return '';
return "";
}
return '';
return "";
}
String get formattedStatus {
@@ -302,7 +359,7 @@ abstract class TransactionDetailsViewModelBase with Store {
return formattedPendingStatus;
}
return transactionInfo.isPending ? S.current.pending : '';
return transactionInfo.isPending ? S.current.pending : "";
}
int get neededConfirmations {
@@ -312,9 +369,11 @@ abstract class TransactionDetailsViewModelBase with Store {
case WalletType.wownero:
return 3;
case WalletType.litecoin:
bool isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
bool fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
if (isPegOut || fromPegOut) return 6;
final isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
final fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
if (isPegOut || fromPegOut) {
return 6;
}
default:
return 0;
}
@@ -322,8 +381,11 @@ abstract class TransactionDetailsViewModelBase with Store {
}
String get formattedTitle {
if (transactionInfo.additionalInfo['autoShield'] == true) {
return "Autoshield";
if (transactionInfo.additionalInfo['isIronwoodMigration'] == true) {
return 'Migration';
}
if (transactionInfo.additionalInfo['isAutoShield'] == true) {
return S.current.shielding;
}
if (transactionInfo.direction == TransactionDirection.incoming) {
return S.current.received;
@@ -339,40 +401,44 @@ abstract class TransactionDetailsViewModelBase with Store {
final txId = transactionInfo.txHash;
if (wallet.chainId != null) {
final explorerUrl = evm!.getExplorerUrlForChainId(wallet.chainId!);
if (explorerUrl != null) return '$explorerUrl/tx/${txId}';
if (explorerUrl != null) {
return "$explorerUrl/tx/${txId}";
}
}
switch (wallet.type) {
case WalletType.monero:
return 'https://monero.com/tx/${txId}';
return "https://monero.com/tx/${txId}";
case WalletType.bitcoin:
return 'https://mempool.space/${wallet.isTestnet ? "testnet/" : ""}tx/${txId}';
return isLightning(transactionInfo)
? "https://sparkscan.io/tx/${txId}"
: 'https://mempool.space/${wallet.isTestnet ? "testnet/" : ""}tx/${txId}';
case WalletType.litecoin:
return bitcoin!.txIsMweb(transactionInfo)
? "https://www.mwebexplorer.com/blocks/block/${transactionInfo.height}"
: 'https://blockchair.com/litecoin/transaction/${txId}';
: "https://blockchair.com/litecoin/transaction/${txId}";
case WalletType.bitcoinCash:
return 'https://blockchair.com/bitcoin-cash/transaction/${txId}';
return "https://blockchair.com/bitcoin-cash/transaction/${txId}";
case WalletType.ethereum:
return 'https://etherscan.io/tx/${txId}';
return "https://etherscan.io/tx/${txId}";
case WalletType.base:
return 'https://basescan.org/tx/${txId}';
return "https://basescan.org/tx/${txId}";
case WalletType.arbitrum:
return 'https://arbiscan.io/tx/${txId}';
return "https://arbiscan.io/tx/${txId}";
case WalletType.bsc:
return 'https://bscscan.com/tx/${txId}';
return "https://bscscan.com/tx/${txId}";
case WalletType.polygon:
return 'https://polygonscan.com/tx/${txId}';
return "https://polygonscan.com/tx/${txId}";
case WalletType.nano:
return 'https://nanexplorer.com/nano/block/${txId}';
return "https://nanexplorer.com/nano/block/${txId}";
case WalletType.banano:
return 'https://nanexplorer.com/banano/block/${txId}';
return "https://nanexplorer.com/banano/block/${txId}";
case WalletType.wownero:
return 'https://explore.wownero.com/tx/${txId}';
return "https://explore.wownero.com/tx/${txId}";
case WalletType.dogecoin:
return 'https://blockchair.com/dogecoin/transaction/${txId}';
return "https://blockchair.com/dogecoin/transaction/${txId}";
case WalletType.none:
return '';
return "";
}
}
@@ -392,46 +458,52 @@ abstract class TransactionDetailsViewModelBase with Store {
: transactionInfo.outputAddresses!.length;
newFee = bitcoin!.getFeeAmountForPriority(
wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount);
wallet,
bitcoin!.getBitcoinTransactionPriorityMedium(),
inputsCount,
outputsCount,
);
RBFListItems.add(
rbfListItems.add(
StandartListItem(
title: S.current.old_fee,
value: tx.feeFormatted() ?? '0.0',
key: ValueKey('standard_list_item_rbf_old_fee_key'),
value: tx.fee?.toStringWithSymbol() ?? "0.0",
key: const ValueKey("standard_list_item_rbf_old_fee_key"),
),
);
if (transactionInfo.fee != null && rawTransaction.isNotEmpty) {
final size = bitcoin!.getTransactionVSize(wallet, rawTransaction);
final recommendedRate = (transactionInfo.fee! / size).round() + 1;
final recommendedRate = (transactionInfo.fee! / BigInt.from(size)) +
transactionInfo.fee!.copyWith(amount: BigInt.one);
RBFListItems.add(
StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte'));
rbfListItems.add(
StandartListItem(title: "New recommended fee rate", value: "$recommendedRate sat/byte"),
);
}
final priorities = priorityForWalletType(wallet.type);
final selectedItem = priorities.indexOf(sendViewModel.feesViewModel.transactionPriority);
final customItem = priorities.firstWhereOrNull(
(element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom);
(element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom,
);
final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null;
final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble();
RBFListItems.add(
rbfListItems.add(
StandardPickerListItem(
key: ValueKey('standard_picker_list_item_transaction_priorities_key'),
key: const ValueKey("standard_picker_list_item_transaction_priorities_key"),
title: S.current.estimated_new_fee,
value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${wallet.currency}',
value: "${bitcoin!.formatterBitcoinAmountToString(amount: newFee)} ${wallet.currency}",
items: priorityForWalletType(wallet.type),
customValue: _appStore.settingsStore.customBitcoinFeeRate.toDouble(),
maxValue: maxCustomFeeRate,
selectedIdx: selectedItem,
customItemIndex: customItemIndex ?? 0,
displayItem: (dynamic priority, double sliderValue) =>
displayItem: (dynamic priority, sliderValue) =>
sendViewModel.feesViewModel.displayFeeRate(priority, sliderValue.round()),
onSliderChanged: (double newValue) =>
setNewFee(value: newValue, priority: transactionPriority!),
onItemSelected: (dynamic item, double sliderValue) {
onSliderChanged: (newValue) => setNewFee(value: newValue, priority: transactionPriority!),
onItemSelected: (dynamic item, sliderValue) {
transactionPriority = item as TransactionPriority;
return setNewFee(value: sliderValue, priority: transactionPriority!);
},
@@ -439,9 +511,9 @@ abstract class TransactionDetailsViewModelBase with Store {
);
if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) {
RBFListItems.add(
rbfListItems.add(
StandardExpandableListItem(
key: ValueKey('standard_expandable_list_item_transaction_input_addresses_key'),
key: const ValueKey("standard_expandable_list_item_transaction_input_addresses_key"),
title: S.current.inputs,
expandableItems: transactionInfo.inputAddresses!,
),
@@ -450,17 +522,17 @@ abstract class TransactionDetailsViewModelBase with Store {
if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) {
final outputAddresses = transactionInfo.outputAddresses!.map((element) {
if (element.contains('OP_RETURN:') && element.length > 40) {
return element.substring(0, 40) + '...';
if (element.contains("OP_RETURN:") && element.length > 40) {
return "${element.substring(0, 40)}...";
}
return element;
}).toList();
RBFListItems.add(
rbfListItems.add(
StandardExpandableListItem(
title: S.current.outputs,
expandableItems: outputAddresses,
key: ValueKey('standard_expandable_list_item_transaction_output_addresses_key'),
key: const ValueKey("standard_expandable_list_item_transaction_output_addresses_key"),
),
);
}
@@ -470,7 +542,7 @@ abstract class TransactionDetailsViewModelBase with Store {
Future<void> _checkForRBF(TransactionInfo tx) async {
if (wallet.type == WalletType.bitcoin &&
transactionInfo.direction == TransactionDirection.outgoing) {
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
final description = transactionDescriptionBox.values
.firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash);
@@ -487,45 +559,23 @@ abstract class TransactionDetailsViewModelBase with Store {
}
}
String setNewFee({double? value, required TransactionPriority priority}) {
String setNewFee({required TransactionPriority priority, double? value}) {
newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null
? bitcoin!.feeAmountWithFeeRate(
wallet,
value.round(),
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1)
transactionInfo.outputAddresses?.length ?? 1,
)
: bitcoin!.getFeeAmountForPriority(
wallet,
priority,
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1);
transactionInfo.outputAddresses?.length ?? 1,
);
return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
}
String get formattedCryptoAmount {
if (wallet.type == WalletType.bitcoin) {
final crypto = isLightning(transactionInfo) ? CryptoCurrency.btcln : CryptoCurrency.btc;
final amount = _appStore.amountParsingProxy
.getDisplayCryptoString(transactionInfo.amount, crypto)
.withMaxDecimals(8)
.withLocalSeperator(_appStore.settingsStore.languageCode);
return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}';
}
return transactionInfo.amountFormatted();
}
void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee);
@computed
String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled
? ''
: sendViewModel.pendingTransactionFiatAmount + ' ' + sendViewModel.fiat.title;
@computed
String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled
? ''
: sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title;
}
@@ -66,7 +66,7 @@ abstract class UnspentCoinsDetailsViewModelBase with Store {
case WalletType.bitcoin:
return 'https://ordinals.com/tx/${txId}';
case WalletType.litecoin:
return 'https://litecoin.earlyordies.com/tx/${txId}';
return 'https://blockchair.com/litecoin/transaction/${txId}';
case WalletType.bitcoinCash:
return 'https://blockchair.com/bitcoin-cash/transaction/${txId}';
case WalletType.dogecoin:
@@ -0,0 +1,9 @@
import 'package:hash_wallet/core/execution_state.dart';
abstract class WalletAccountEditOrCreateViewModel {
bool get isEdit;
ExecutionState get state;
String get label;
set label(String value);
Future<void> save();
}
@@ -0,0 +1,17 @@
import "package:cw_core/amount/money.dart";
class AccountListItem {
AccountListItem({
required this.label,
required this.id,
required this.balance,
this.isSelected = false,
});
final String label;
final int id;
final bool isSelected;
final Money balance;
bool get isFunded => balance.sign > 0;
}
@@ -0,0 +1,92 @@
import 'dart:math';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart';
import 'package:cw_core/balance_card_style_settings.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
part 'bitcoin_account_edit_or_create_view_model.g.dart';
class BitcoinAccountEditOrCreateViewModel = BitcoinAccountEditOrCreateViewModelBase
with _$BitcoinAccountEditOrCreateViewModel;
abstract class BitcoinAccountEditOrCreateViewModelBase
with Store
implements WalletAccountEditOrCreateViewModel {
BitcoinAccountEditOrCreateViewModelBase({required WalletBase wallet})
: state = InitialExecutionState(),
label = "",
_wallet = wallet;
@override
bool get isEdit => false;
@override
@observable
ExecutionState state;
@override
@observable
String label;
final WalletBase _wallet;
Future<List<Gradient>> _getUsableCardGradients() async {
final List<Gradient> ret = List<Gradient>.from(CardDesign.allGradients);
final designs = (await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId))
.map((item) => CardDesign.fromStyleSettings(item, _wallet.currency));
for (final design in designs) {
ret.remove(design.gradient);
}
return ret.isNotEmpty ? ret : CardDesign.allGradients;
}
Future<void> _saveRandomCardDesign(int accountIndex) async {
final gradients = await _getUsableCardGradients();
final accounts = await _wallet.walletInfo.getAccounts();
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: accountIndex,
cardOrder: accounts.length - 1,
design: CardDesign.specialDesignsForCurrencies[_wallet.currency]!
.withGradient(gradients[Random().nextInt(gradients.length)]))
.insert();
}
@override
Future<void> save() async {
try {
state = IsExecutingState();
final accountIndex = await _createAccount(label);
await _saveRandomCardDesign(accountIndex);
await _wallet.save();
state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
}
}
Future<int> _createAccount(String label) async {
final accounts = await _wallet.walletInfo.getAccounts();
final nextIndex = accounts.isEmpty
? 0
: accounts.map((e) => e.accountIndex).reduce((a, b) => a > b ? a : b) + 1;
await _wallet.walletInfo.addAccount(
accountIndex: nextIndex,
label: label.isEmpty ? "Account $nextIndex" : label,
);
await bitcoin!.setCurrentAccount(_wallet, nextIndex);
return nextIndex;
}
}
@@ -0,0 +1,107 @@
import "dart:async";
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
import "package:collection/collection.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_info.dart";
import "package:mobx/mobx.dart";
part "bitcoin_account_list_view_model.g.dart";
class BitcoinAccountListViewModel = BitcoinAccountListViewModelBase
with _$BitcoinAccountListViewModel;
abstract class BitcoinAccountListViewModelBase with Store implements WalletAccountListViewModel {
BitcoinAccountListViewModelBase(this._wallet) {
unawaited(_loadAccounts());
reaction(
(_) => bitcoin!.accountBalancesSnapshot(_wallet),
(snapshot) => unawaited(_refreshBalances(snapshot)),
equals: const MapEquality<int, Object>().equals,
);
}
final WalletBase _wallet;
List<WalletInfoAccount>? _cachedStoredAccounts;
Future<void>? _loadAccountsFuture;
CryptoCurrency get currency => _wallet.currency;
@override
@observable
ObservableList<AccountListItem> accounts = ObservableList<AccountListItem>();
@override
@observable
AccountListItem? selectedAccount;
@override
@action
Future<void> select(AccountListItem account) async {
await bitcoin!.setCurrentAccount(_wallet, account.id);
runInAction(() => selectedAccount = account);
await reload();
}
@action
Future<void> _loadAccounts() => _runLoad(() => _wallet.walletInfo.getAccounts());
@action
Future<void> _refreshBalances(Map<int, Object> balancesSnapshot) {
final cached = _cachedStoredAccounts;
final hasUnknownAccount = cached == null ||
balancesSnapshot.keys
.any((accountIndex) => !cached.any((a) => a.accountIndex == accountIndex));
if (hasUnknownAccount) return _loadAccounts();
return _runLoad(() async => cached);
}
Future<void> _runLoad(Future<List<WalletInfoAccount>> Function() fetch) {
final inProgress = _loadAccountsFuture;
if (inProgress != null) {
return inProgress;
}
final future = _applyAccounts(fetch);
_loadAccountsFuture = future;
return future.whenComplete(() => _loadAccountsFuture = null);
}
Future<void> _applyAccounts(Future<List<WalletInfoAccount>> Function() fetch) async {
final storedAccounts = await fetch();
_cachedStoredAccounts = storedAccounts;
final currentAccountIndex = _wallet.walletInfo.currentAccountIndex;
final items = storedAccounts
.map((account) => AccountListItem(
id: account.accountIndex,
label: account.label,
balance: _fullBalance(account.accountIndex),
isSelected: account.accountIndex == currentAccountIndex,
))
.toList();
runInAction(() {
accounts = ObservableList<AccountListItem>.of(items);
selectedAccount = items.isEmpty
? null
: items.firstWhere(
(account) => account.isSelected,
orElse: () => items.first,
);
});
}
Money _fullBalance(int accountIndex) => bitcoin!.balanceForAccount(_wallet, accountIndex);
@override
@action
Future<void> reload() => _loadAccounts();
}
@@ -1,6 +1,8 @@
import 'dart:math';
import 'package:hash_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/balance_card_layout.dart';
import 'package:cw_core/balance_card_style_settings.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/wallet_base.dart';
@@ -9,14 +11,16 @@ import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:hash_wallet/view_model/wallet_account_list/account_list_item.dart';
part 'monero_account_edit_or_create_view_model.g.dart';
class MoneroAccountEditOrCreateViewModel = MoneroAccountEditOrCreateViewModelBase
with _$MoneroAccountEditOrCreateViewModel;
abstract class MoneroAccountEditOrCreateViewModelBase with Store {
abstract class MoneroAccountEditOrCreateViewModelBase
with Store
implements WalletAccountEditOrCreateViewModel {
MoneroAccountEditOrCreateViewModelBase(this._moneroAccountList, this._wowneroAccountList,
{required WalletBase wallet, AccountListItem? accountListItem})
: state = InitialExecutionState(),
@@ -33,7 +37,7 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store {
@observable
String label;
final MoneroAccountList _moneroAccountList;
final MoneroAccountList? _moneroAccountList;
final WowneroAccountList? _wowneroAccountList;
final AccountListItem? _accountListItem;
final WalletBase _wallet;
@@ -48,62 +52,77 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store {
return ret.isNotEmpty ? ret : CardDesign.allGradients;
}
Future<void> _saveRandomCardDesign() async {
Future<void> _saveRandomCardDesign(int accountIndex) async {
final gradients = await _getUsableCardGradients();
final accountIndices = _wallet.type == WalletType.monero
? _moneroAccountList!.accounts.map((account) => account.id).toList()
: _wowneroAccountList!.accounts.map((account) => account.id).toList();
final layout = BalanceCardLayout.resolve(
accountIndices: accountIndices,
settings: await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId),
);
await BalanceCardStyleSettings.fromCardDesign(
_wallet.walletInfo.internalId,
_moneroAccountList.accounts.length,
_moneroAccountList.accounts.length,
CardDesign.specialDesignsForCurrencies[_wallet.currency]!
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: accountIndex,
cardOrder: layout.visible.length,
design: CardDesign.specialDesignsForCurrencies[_wallet.currency]!
.withGradient(gradients[Random().nextInt(gradients.length)]))
.insert();
}
Future<void> save() async {
await _saveRandomCardDesign();
if (_wallet.type == WalletType.monero) {
await saveMonero();
}
try {
state = IsExecutingState();
if (_wallet.type == WalletType.wownero) {
await saveWownero();
if (!isEdit) {
await _saveRandomCardDesign(_wallet.type == WalletType.monero
? _moneroAccountList!.accounts.length
: _wowneroAccountList!.accounts.length);
}
if (_wallet.type == WalletType.monero) {
await saveMonero();
} else if (_wallet.type == WalletType.wownero) {
await saveWownero();
}
await _wallet.save();
state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
}
}
Future<void> saveMonero() async {
try {
state = IsExecutingState();
if (_accountListItem != null) {
await _moneroAccountList.setLabelAccount(_wallet,
accountIndex: _accountListItem.id, label: label);
} else {
await _moneroAccountList.addAccount(_wallet, label: label);
}
await _wallet.save();
state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
if (_accountListItem != null) {
await _moneroAccountList!.setLabelAccount(
_wallet,
accountIndex: _accountListItem.id,
label: label,
);
} else {
await _moneroAccountList!.addAccount(
_wallet,
label: label,
);
}
}
Future<void> saveWownero() async {
try {
state = IsExecutingState();
if (_accountListItem != null) {
await _wowneroAccountList?.setLabelAccount(_wallet,
accountIndex: _accountListItem.id, label: label);
} else {
await _wowneroAccountList?.addAccount(_wallet, label: label);
}
await _wallet.save();
state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
if (_accountListItem != null) {
await _wowneroAccountList?.setLabelAccount(
_wallet,
accountIndex: _accountListItem.id,
label: label,
);
} else {
await _wowneroAccountList?.addAccount(
_wallet,
label: label,
);
}
}
}
@@ -0,0 +1,79 @@
import "package:hash_wallet/monero/monero.dart";
import "package:hash_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
import "package:hash_wallet/wownero/wownero.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_type.dart";
import "package:mobx/mobx.dart";
part "monero_account_list_view_model.g.dart";
class MoneroAccountListViewModel = MoneroAccountListViewModelBase with _$MoneroAccountListViewModel;
abstract class MoneroAccountListViewModelBase with Store implements WalletAccountListViewModel {
MoneroAccountListViewModelBase(this._wallet);
@override
AccountListItem? get selectedAccount => selected;
CryptoCurrency get currency => _wallet.currency;
@computed
List<AccountListItem> get accounts {
if (_wallet.type == WalletType.monero) {
return monero!
.getAccountList(_wallet)
.accounts
.map((acc) => AccountListItem(
label: acc.label,
id: acc.id,
balance: monero!.getAccountFullBalance(acc.id),
isSelected: acc.id == monero!.getCurrentAccount(_wallet).id))
.toList();
}
if (_wallet.type == WalletType.wownero) {
return wownero!
.getAccountList(_wallet)
.accounts
.map((acc) => AccountListItem(
label: acc.label,
id: acc.id,
balance: wownero!.getAccountFullBalance(acc.id),
isSelected: acc.id == wownero!.getCurrentAccount(_wallet).id))
.toList();
}
throw Exception("Unexpected wallet type: ${_wallet.type} for monero");
}
@computed
AccountListItem get selected => accounts.firstWhere((item) => item.isSelected);
final WalletBase _wallet;
@override
Future<void> reload() async {}
@override
Future<void> select(AccountListItem item) async {
if (_wallet.type == WalletType.monero) {
monero!.setCurrentAccount(
_wallet,
item.id,
item.label,
item.balance.toString(),
);
}
if (_wallet.type == WalletType.wownero) {
wownero!.setCurrentAccount(
_wallet,
item.id,
item.label,
item.balance.toString(),
);
}
}
}
@@ -0,0 +1,14 @@
import "package:hash_wallet/view_model/wallet_account_list/account_list_item.dart";
import "package:cw_core/crypto_currency.dart";
abstract class WalletAccountListViewModel {
List<AccountListItem> get accounts;
AccountListItem? get selectedAccount;
CryptoCurrency get currency;
Future<void> select(AccountListItem account);
Future<void> reload();
}
@@ -1,51 +1,49 @@
import 'dart:core';
import 'dart:developer' as dev;
import "dart:core";
import "dart:developer" as dev;
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/core/fiat_conversion_service.dart';
import 'package:hash_wallet/core/wallet_change_listener_view_model.dart';
import 'package:hash_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:hash_wallet/entities/fiat_api_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/monero/monero.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/reactions/wallet_utils.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:hash_wallet/store/yat/yat_store.dart';
import 'package:hash_wallet/utils/list_item.dart';
import 'package:hash_wallet/utils/qr_util.dart';
import 'package:hash_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart';
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_util.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/core/amount_parsing_proxy.dart";
import "package:hash_wallet/core/fiat_conversion_service.dart";
import "package:hash_wallet/core/wallet_change_listener_view_model.dart";
import "package:hash_wallet/entities/auto_generate_subaddress_status.dart";
import "package:hash_wallet/entities/fiat_api_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/monero/monero.dart";
import "package:hash_wallet/reactions/wallet_connect.dart";
import "package:hash_wallet/reactions/wallet_utils.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:hash_wallet/store/dashboard/fiat_conversion_store.dart";
import "package:hash_wallet/utils/list_item.dart";
import "package:hash_wallet/utils/qr_util.dart";
import "package:hash_wallet/view_model/wallet_address_list/wallet_account_list_header.dart";
import "package:hash_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart";
import "package:hash_wallet/view_model/wallet_address_list/wallet_address_list_header.dart";
import "package:hash_wallet/view_model/wallet_address_list/wallet_address_list_item.dart";
import "package:hash_wallet/view_model/wallet_address_list/wallet_address_util.dart";
import "package:hash_wallet/wownero/wownero.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/erc20_token.dart";
import "package:cw_core/payment_uris.dart";
import "package:cw_core/wallet_type.dart";
import "package:mobx/mobx.dart";
part 'wallet_address_list_view_model.g.dart';
part "wallet_address_list_view_model.g.dart";
class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel;
abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store {
WalletAddressListViewModelBase({
required AppStore appStore,
required this.yatStore,
required super.appStore,
required this.fiatConversionStore,
}) : _baseItems = <ListItem>[],
selectedCurrency = appStore.wallet!.currency,
hasAccounts = [WalletType.monero, WalletType.wownero].contains(appStore.wallet!.type),
_appStore = appStore,
receivePageOption = appStore.wallet!.walletAddresses.walletInfo.addressPageType ?? '',
super(appStore: appStore) {
receivePageOption = appStore.wallet!.walletAddresses.walletInfo.addressPageType ?? "" {
_init();
}
@@ -73,11 +71,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
CryptoCurrency? tokenCurrency;
@computed
String get cryptoCurrencySymbol =>
_appStore.amountParsingProxy.getCryptoSymbol(tokenCurrency ?? wallet.currency);
String get cryptoCurrencySymbol => _appStore.amountParsingProxy.getCryptoSymbol(
tokenCurrency ??
(selectedCurrency is CryptoCurrency
? selectedCurrency as CryptoCurrency
: wallet.currency),
);
void setTokenCurrency(Currency curr) {
if (curr == wallet.currency || curr == CryptoCurrency.btcln) {
if (curr == CryptoCurrency.btcln) {
tokenCurrency = null;
selectedCurrency = CryptoCurrency.btcln;
return;
}
if (curr == wallet.currency) {
tokenCurrency = null;
selectedCurrency = wallet.currency;
return;
@@ -114,7 +121,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
_appStore.amountParsingProxy.useSatoshi(selectedCurrency as CryptoCurrency);
@observable
String searchText = '';
String searchText = "";
@computed
int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency);
@@ -123,30 +130,45 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
int get tokenCurrencyIndex => tokenCurrency == null ? 0 : tokenCurrencies.indexOf(tokenCurrency!);
@observable
String _amount = '';
String _amount = "";
@computed
String get displayAmount => _appStore.amountParsingProxy
.getDisplayCryptoAmount(_amount, tokenCurrency ?? wallet.currency);
String get displayAmount => _appStore.amountParsingProxy.getDisplayCryptoAmount(
_amount,
tokenCurrency ??
(selectedCurrency is CryptoCurrency
? selectedCurrency as CryptoCurrency
: wallet.currency),
);
// NOT PRECISE! just for display purposes.
@computed
String get fiatAmount {
if (_amount.isEmpty) return "";
if (_amount.isEmpty) {
return "";
}
var cryptoCurrency = tokenCurrency ?? wallet.currency;
if (cryptoCurrency == CryptoCurrency.btcln) cryptoCurrency = CryptoCurrency.btc;
if (cryptoCurrency == CryptoCurrency.btcln) {
cryptoCurrency = CryptoCurrency.btc;
}
if (selectedCurrency is FiatCurrency && _fiatRate != null) {
return selectedCurrencyFiatAmount;
}
if (!fiatConversionStore.prices.containsKey(cryptoCurrency)) return "";
if (!fiatConversionStore.prices.containsKey(cryptoCurrency)) {
return "";
}
final amount = double.tryParse(_amount) ?? 0;
return (amount * fiatConversionStore.prices[cryptoCurrency]!).toStringAsFixed(2);
}
@computed
String get selectedCurrencyFiatAmount {
if (_fiatRate == null) return "";
if (_fiatRate == null) {
return "";
}
final amount = double.tryParse(_amount) ?? 0;
return (amount * _fiatRate!).toStringAsFixed(2);
}
@@ -164,6 +186,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
!isSilentPayments &&
uri.toString().contains("payjo.in");
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
@computed
FiatCurrency get fiatCurrency => _appStore.settingsStore.fiatCurrency;
@@ -190,13 +214,18 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@computed
PaymentURI get uri {
if (tokenCurrency != null && isEVMCompatibleChain(wallet.type)) {
final erc20Token = tokenCurrency! as Erc20Token;
return ERC681URI(
chainId: wallet.chainId ?? 1,
address: wallet.walletAddresses.address,
amount: _amount,
contractAddress: (tokenCurrency as Erc20Token).contractAddress);
chainId: wallet.chainId ?? 1,
address: wallet.walletAddresses.address,
amount: _amount,
contractAddress: erc20Token.contractAddress,
tokenDecimals: erc20Token.decimal,
);
}
if (isLightning && _lnPaymentRequest != null) {
return _lnPaymentRequest!;
}
if (isLightning && _lnPaymentRequest != null) return _lnPaymentRequest!;
return wallet.walletAddresses.getPaymentUri(_amount);
}
@@ -211,7 +240,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
final addressList = ObservableList<ListItem>();
if (wallet.type == WalletType.monero) {
final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first;
final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.firstOrNull;
final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) {
final isPrimary = subaddress == primaryAddress;
@@ -233,10 +262,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
final isPrimary = subaddress == primaryAddress;
return WalletAddressListItem(
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.label,
address: subaddress.address);
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.label,
address: subaddress.address,
);
});
addressList.addAll(addressItems);
}
@@ -261,37 +291,37 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
addressList.addAll(addressItems);
addressList.add(WalletAddressListHeader(title: S.current.received));
final receivedAddressItems =
bitcoin!.getSilentPaymentReceivedAddresses(wallet).map((address) {
return WalletAddressListItem(
id: address.id,
isPrimary: false,
name: address.name,
address: address.address,
txCount: address.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)),
isChange: address.isChange,
isOneTimeReceiveAddress: true,
derivationPath: address.derivationPath,
);
});
final receivedAddressItems = bitcoin!.getSilentPaymentReceivedAddresses(wallet).map(
(address) => WalletAddressListItem(
id: address.id,
isPrimary: false,
name: address.name,
address: address.address,
txCount: address.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)),
isChange: address.isChange,
isOneTimeReceiveAddress: true,
derivationPath: address.derivationPath,
),
);
addressList.addAll(receivedAddressItems);
} else {
var addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
final isPrimary = subaddress.id == 0;
return WalletAddressListItem(
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.name,
address: subaddress.address,
txCount: subaddress.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)),
isChange: subaddress.isChange,
isLegacyDerivation: subaddress.isLegacyDerivation,
derivationPath: subaddress.derivationPath);
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.name,
address: subaddress.address,
txCount: subaddress.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)),
isChange: subaddress.isChange,
isLegacyDerivation: subaddress.isLegacyDerivation,
derivationPath: subaddress.derivationPath,
);
});
// don't show all 1000+ mweb addresses:
@@ -316,46 +346,52 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
}
if (wallet.type == WalletType.nano) {
addressList.add(WalletAddressListItem(
isPrimary: true,
name: null,
address: wallet.walletAddresses.address,
));
addressList.add(
WalletAddressListItem(
isPrimary: true,
name: null,
address: wallet.walletAddresses.address,
),
);
}
for (var i = 0; i < addressList.length; i++) {
if (!(addressList[i] is WalletAddressListItem)) continue;
if (addressList[i] is! WalletAddressListItem) {
continue;
}
final item = addressList[i] as WalletAddressListItem;
item.isHidden = wallet.walletAddresses.hiddenAddresses.contains(item.address) ||
(isElectrumWallet && item.isLegacyDerivation);
}
for (var i = 0; i < addressList.length; i++) {
if (!(addressList[i] is WalletAddressListItem)) continue;
if (addressList[i] is! WalletAddressListItem) {
continue;
}
(addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses
.contains((addressList[i] as WalletAddressListItem).address);
}
if (searchText.isNotEmpty) {
return ObservableList.of(addressList.where((item) {
if (item is WalletAddressListItem) {
return item.address.toLowerCase().contains(searchText.toLowerCase());
}
return false;
}));
return ObservableList.of(
addressList.where((item) {
if (item is WalletAddressListItem) {
return item.address.toLowerCase().contains(searchText.toLowerCase());
}
return false;
}),
);
}
return addressList;
}
@computed
ObservableList<ListItem> get addressList {
return _computeAddressList();
}
ObservableList<ListItem> get addressList => _computeAddressList();
List<ListItem> get forceRecomputeItems {
// necessary because the addressList contains non-observable items
List<ListItem> recomputed = [];
final recomputed = <ListItem>[];
recomputed.addAll(_baseItems);
recomputed.addAll(_computeAddressList());
return recomputed;
@@ -393,9 +429,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
case WalletType.wownero:
return wownero!.getCurrentAccount(wallet).label;
default:
return '';
return "";
}
return '';
return "";
}
@computed
@@ -424,7 +460,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.dogecoin
WalletType.dogecoin,
].contains(wallet.type);
List<String> getWalletImages(int? chainId) {
@@ -432,44 +468,50 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
switch (chainId) {
case 1:
return [
'assets/new-ui/crypto_full_icons/ethereum.svg',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
'assets/images/deuro_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/ethereum.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
"assets/images/deuro_icon.svg",
"assets/images/more_tokens.svg",
];
case 137:
return [
'assets/images/crypto/polygon.webp',
'assets/images/eth_pol_icon.svg',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/polygon.svg",
"assets/images/eth_pol_icon.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
"assets/images/more_tokens.svg",
];
case 8453:
return [
'assets/new-ui/crypto_full_icons/ethereum.svg',
'assets/images/usdc_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/ethereum.svg",
"assets/images/usdc_icon.svg",
"assets/images/more_tokens.svg",
];
case 42161:
return [
'assets/images/crypto/arbitrum.webp',
'assets/images/usdc_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/arbitrum.svg",
"assets/images/usdc_icon.svg",
"assets/images/more_tokens.svg",
];
case 56:
return [
'assets/images/crypto/BNB.webp',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/bnb.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
"assets/images/more_tokens.svg",
];
case 4663:
return [
"assets/new-ui/crypto_full_icons/robinhood.svg",
"assets/images/usdg_icon.png",
"assets/images/more_tokens.svg",
];
default:
return [
'assets/images/crypto/ethereum.webp',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
"assets/new-ui/crypto_full_icons/ethereum.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
];
}
}
@@ -482,7 +524,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@computed
String get qrImage {
if (isLightning) return 'assets/images/btc_chain_qr_lightning.svg';
if (isLightning) {
return "assets/images/btc_chain_qr_lightning.svg";
}
return getQrImage(type);
}
@@ -502,7 +546,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@computed
bool get isLightning =>
wallet.type == WalletType.bitcoin &&
(wallet.walletAddresses.getPaymentUri(_amount) is LightningPaymentRequest);
(selectedCurrency == CryptoCurrency.btcln || bitcoin!.hasSelectedLightning(wallet));
@observable
String receivePageOption;
@@ -525,17 +569,25 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
List<ListItem> _baseItems;
final YatStore yatStore;
@action
void setAddress(WalletAddressListItem address) =>
wallet.walletAddresses.address = address.address;
@observable
bool isRotatingAddress = false;
@action
Future<void> rotateAddress() async {
await createNewAddress(wallet, "");
if (isElectrumWallet) {
if (isRotatingAddress) {
return;
}
try {
isRotatingAddress = true;
await createNewAddress(wallet, "");
wallet.walletAddresses.address = addressList.whereType<WalletAddressListItem>().last.address;
} finally {
isRotatingAddress = false;
}
}
@@ -595,16 +647,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@action
void changeAmount(String amount) {
if (selectedCurrency is FiatCurrency) {
this._amount = amount;
_amount = amount;
_convertAmountToCrypto();
} else if (selectedCurrency is CryptoCurrency) {
this._amount = _appStore.amountParsingProxy
_amount = _appStore.amountParsingProxy
.getCanonicalCryptoAmount(amount, selectedCurrency as CryptoCurrency);
}
if (isLightning) {
wallet.walletAddresses
.getPaymentRequestUri(this._amount)
.then((uri) => _lnPaymentRequest = uri);
wallet.walletAddresses.getPaymentRequestUri(_amount).then((uri) => _lnPaymentRequest = uri);
}
}
@@ -616,22 +666,24 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@action
void _convertAmountToCrypto() {
var cryptoCurrency = tokenCurrency ?? wallet.currency;
if (cryptoCurrency == CryptoCurrency.btcln) cryptoCurrency = CryptoCurrency.btc;
if (cryptoCurrency == CryptoCurrency.btcln) {
cryptoCurrency = CryptoCurrency.btc;
}
final fiatRate = _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0);
if (fiatRate <= 0.0) {
dev.log("Invalid Fiat Rate $fiatRate");
_amount = '';
_amount = "";
return;
}
try {
final crypto = (double.parse(_amount.replaceAll(',', '.')) / fiatRate).toStringAsFixed(8);
final crypto = (double.parse(_amount.replaceAll(",", ".")) / fiatRate).toStringAsFixed(8);
if (_amount != crypto) {
_amount = crypto;
}
} catch (e) {
_amount = '';
_amount = "";
}
}
@@ -19,11 +19,14 @@ Future<void> createNewAddress(WalletBase wallet, String label) async {
await monero!
.getSubaddressList(wallet)
.addSubaddress(wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label);
final addr = await monero!
.getSubaddressList(wallet)
.subaddresses
.first
.address; // first because the order is reversed
final subaddressList = await monero!.getSubaddressList(wallet).subaddresses;
if (subaddressList.isEmpty) {
// this shouldn't happen, we just added an addr.
// somehow, it happened once in prod regardless.
// we just return instead of crashing, user can press the button again ig
return;
}
final addr = subaddressList.first.address; // first because the order is reversed
wallet.walletAddresses.manualAddresses.add(addr);
await wallet.save();
}
+8
View File
@@ -33,9 +33,13 @@ abstract class WalletCreationVMBase with Store {
@observable
bool _useTestnet = false;
int _zcashNetwork = 0; // Hash Bags: Zcash removed; kept for the shared credentials signature
@computed
bool get useTestnet => _useTestnet;
int get zcashNetwork => _zcashNetwork;
@observable
String name;
@@ -251,4 +255,8 @@ abstract class WalletCreationVMBase with Store {
void toggleUseTestnet(bool? value) {
_useTestnet = value ?? !_useTestnet;
}
void setZcashNetwork(int network) {
_zcashNetwork = network;
}
}
@@ -128,8 +128,11 @@ abstract class WalletGroupsDisplayViewModelBase with Store {
bool isNonSeedWallet = wallet.isNonSeedWallet;
bool isNotMoneroBip39Wallet =
wallet.type == WalletType.monero && di.derivationType != DerivationType.bip39;
bool isNotBip39CoinWallet =
wallet.type == WalletType.monero &&
di.derivationType != DerivationType.bip39;
bool isNotDecredBip39Wallet = false;
// Exclude if any of these conditions are true
shouldExcludeGroup = shouldExcludeGroup ||
@@ -138,7 +141,8 @@ abstract class WalletGroupsDisplayViewModelBase with Store {
isElectrumDerivationType ||
isSameTypeAsSelectedWallet ||
isNonSeedWallet ||
isNotMoneroBip39Wallet;
isNotBip39CoinWallet ||
isNotDecredBip39Wallet;
}
if (shouldExcludeGroup) continue;
@@ -6,13 +6,14 @@ import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:hash_wallet/view_model/seed_settings_view_model.dart';
import 'package:hash_wallet/view_model/wallet_creation_vm.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -43,6 +44,8 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
@observable
String? error = null;
bool get passphraseAvailable => hardwareWalletVM.hardwareWalletType == HardwareWalletType.trezor;
// @observable
ObservableList<HardwareAccountData> availableAccounts = ObservableList();
@@ -54,7 +57,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
availableAccounts.addAll(accounts);
_nextIndex += limit;
} catch (e) {
} on Exception catch (e) {
printV(e);
error =
hardwareWalletVM.interpretErrorCode(e.toString()) ?? S.current.ledger_connection_error;
@@ -84,9 +87,10 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
credentials = monero!.createMoneroRestoreWalletFromHardwareCredentials(
name: name,
ledgerConnection: (hardwareWalletVM as LedgerViewModel).connection,
hardwareWalletService: hardwareWalletVM.getHardwareWalletService(type),
password: password,
height: _options['height'] as int? ?? 0,
passphrase: _options['passphrase'] as String?,
);
default:
throw Exception('Unexpected type: ${type.toString()}');
@@ -100,6 +104,12 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
@override
Future<WalletBase> process(WalletCredentials credentials) async {
walletCreationService.changeWalletType(type: type);
return walletCreationService.restoreFromHardwareWallet(credentials);
final wallet = await walletCreationService.restoreFromHardwareWallet(credentials);
final hardwareWalletVM = this.hardwareWalletVM;
if (hardwareWalletVM is TrezorConnectViewModel && type == WalletType.bitcoin) {
hardwareWalletVM.bindSessionToWallet(wallet);
}
return wallet;
}
}
+11 -5
View File
@@ -9,12 +9,9 @@ import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:polyseed/polyseed.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
part 'wallet_keys_view_model.g.dart';
@@ -27,6 +24,7 @@ abstract class WalletKeysViewModelBase with Store {
_restoreHeight = _appStore.wallet!.walletInfo.restoreHeight,
_restoreHeightByTransactions = 0,
items = ObservableList<StandartListItem>(),
silentPaymentItems = ObservableList<StandartListItem>(),
_title = _getInitialTitle(_appStore.wallet!) {
_populateKeysItems();
@@ -82,6 +80,7 @@ abstract class WalletKeysViewModelBase with Store {
// this is incomplete, needs legacy seed toggle for XMR
bool get shouldShowHeightBox => [WalletType.bitcoin].contains(_wallet.type);
final ObservableList<StandartListItem> items;
final ObservableList<StandartListItem> silentPaymentItems;
@observable
String _title;
@@ -111,7 +110,7 @@ abstract class WalletKeysViewModelBase with Store {
final langName = PolyseedLang.getByPhrase(_wallet.seed!).nameEnglish;
if (_wallet.type == WalletType.monero) {
return (_wallet as MoneroWalletBase).seedLegacy(langName);
return monero!.getLegacySeed(_wallet, langName);
} else if (_wallet.type == WalletType.wownero) {
return wownero!.getLegacySeed(_wallet, langName);
}
@@ -145,6 +144,7 @@ abstract class WalletKeysViewModelBase with Store {
void _populateKeysItems() {
items.clear();
silentPaymentItems.clear();
Map<String, String>? keys;
@@ -198,6 +198,11 @@ abstract class WalletKeysViewModelBase with Store {
final electrumKeys = bitcoin!.getWalletKeys(_appStore.wallet!);
items.addAll([
if ((electrumKeys['masterFingerprint'] ?? '').isNotEmpty)
StandartListItem(
title: "Master fingerprint",
value: electrumKeys['masterFingerprint']!,
),
if ((electrumKeys['wif'] ?? '').isNotEmpty)
StandartListItem(title: "WIF", value: electrumKeys['wif']!),
if ((electrumKeys['privateKey'] ?? '').isNotEmpty)
@@ -213,7 +218,8 @@ abstract class WalletKeysViewModelBase with Store {
}
if (keys != null) {
items.addAll([
final keysList = _wallet.type == WalletType.bitcoin ? silentPaymentItems : items;
keysList.addAll([
if ((keys['primaryAddress'] ?? '').isNotEmpty)
StandartListItem(
key: ValueKey('${_walletName}_wallet_primary_address_item_key'),
+1
View File
@@ -122,6 +122,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
@override
Future<WalletBase> process(WalletCredentials credentials) async {
walletCreationService.changeWalletType(type: type);
credentials.walletInfo!.showSeedBackupReminder = newWalletArguments!.mnemonic == null;
return walletCreationService.create(credentials, isTestnet: useTestnet);
}
}
@@ -92,6 +92,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
@observable
WalletRestoreMode mode;
@computed
bool get passphraseAvailable => mode == WalletRestoreMode.seed;
@observable
bool hasPassphrase;
@@ -1,7 +1,9 @@
import "dart:async";
import 'dart:math';
import 'package:hash_wallet/utils/feature_flag.dart';
import 'package:cw_core/utils/print_verbose.dart';
import "package:cw_core/wallet_info.dart";
import 'package:flutter/foundation.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -16,11 +18,14 @@ abstract class WalletSeedViewModelBase with Store {
: name = wallet.name,
seed = wallet.seed!,
walletType = wallet.type,
_walletInfo = wallet.walletInfo,
currentOptions = ObservableList<String>(),
verificationIndices = ObservableList<int>() {
setupSeedVerification();
}
final WalletInfo _walletInfo;
@observable
String name;
@@ -133,6 +138,7 @@ abstract class WalletSeedViewModelBase with Store {
} else {
// All verification steps completed
isVerificationComplete = true;
unawaited(markSeedVerified());
}
return true;
@@ -141,4 +147,12 @@ abstract class WalletSeedViewModelBase with Store {
return false;
}
}
Future<void> markSeedVerified() async {
if (!_walletInfo.showSeedBackupReminder) {
return;
}
await _walletInfo.clearSeedBackupReminder();
}
}
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/core/wallet_loading_service.dart';
import 'package:cw_core/wallet_base.dart';
@@ -32,13 +31,6 @@ abstract class WalletSwitcherViewModelBase with Store {
Future<List<WalletInfo>> getWallets(WalletType? walletType) async {
final wiList = await WalletInfo.getAll();
if (walletType == null) return wiList;
// For EVM-compatible wallet types, show all EVM-compatible wallets
// This allows users to switch between any EVM wallet regardless of the specific chain
if (isEVMCompatibleChain(walletType)) {
return wiList.where((wallet) => isEVMCompatibleChain(wallet.type)).toList();
}
return wiList.where((wallet) => wallet.type == walletType).toList();
}