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