Merge upstream Cake Wallet v6.5.0

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

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

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

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
+85 -94
View File
@@ -1,25 +1,24 @@
import 'dart:math' show min;
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/di.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount_raw.dart';
import 'package:hash_wallet/entities/contact_base.dart';
import 'package:hash_wallet/entities/parse_address_from_domain.dart';
import 'package:hash_wallet/entities/parsed_address.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/format_fixed.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -45,12 +44,13 @@ abstract class OutputBase with Store {
note = '',
memo = "",
extractedAddress = '',
estimatedFee = '0.0',
parsedAddress = ParsedAddress(addresses: []) {
estimatedFee = Money.zero(cryptoCurrencyHandler()),
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) {
autorun((_) {
final status = _wallet.syncStatus;
printV("Sync status changed to $status. Recalculating fees");
if (status is! SyncedSyncStatus) {
return;
}
calculateEstimatedFee();
});
}
@@ -59,6 +59,9 @@ abstract class OutputBase with Store {
bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler());
@observable
bool isFiatEntry = false;
@observable
String fiatAmount;
@@ -69,8 +72,7 @@ abstract class OutputBase with Store {
String? displayName;
@computed
String get displayCryptoAmount =>
_appStore.amountParsingProxy.getDisplayCryptoAmount(cryptoAmount, cryptoCurrencyHandler());
String get displayCryptoAmount => _appStore.amountParsingProxy.asDisplayString(cryptoAmountMoney);
@observable
String cryptoFullBalance;
@@ -95,14 +97,13 @@ abstract class OutputBase with Store {
@computed
bool get isParsedAddress =>
parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty;
parsedAddress.addressSource != AddressSource.notParsed && parsedAddress.handle.isNotEmpty;
String roundedCryptoAmount(int digits) => displayCryptoAmount.withMaxDecimals(digits);
String roundedFiatAmount(int digits) {
if (fiatAmount.split(".").last.length <= digits) {
return fiatAmount;
}
if (fiatAmount.split(".").last.length <= digits) return fiatAmount;
return double.tryParse(fiatAmount.replaceAll(",", ""))?.toStringAsPrecision(digits) ?? '0.0';
}
@@ -110,52 +111,18 @@ abstract class OutputBase with Store {
String? stealthAddress;
@computed
int get formattedCryptoAmount {
int amount = 0;
Money get cryptoAmountMoney {
if (cryptoAmount.isEmpty) return Money.zero(cryptoCurrencyHandler());
try {
if (cryptoAmount.isNotEmpty) {
final _cryptoAmount = cryptoAmount.replaceAll(',', '.');
int _amount = 0;
switch (walletType) {
case WalletType.monero:
_amount = monero!.formatterMoneroParseAmount(amount: _cryptoAmount);
break;
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.bitcoinCash:
case WalletType.dogecoin:
_amount = cryptoCurrencyHandler().parseAmount(_cryptoAmount).toInt();
break;
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
_amount = evm!.formatterEVMParseAmount(_cryptoAmount);
break;
case WalletType.wownero:
_amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount);
break;
case WalletType.none:
case WalletType.nano:
case WalletType.banano:
break;
}
if (_amount > 0) {
amount = _amount;
}
}
return cryptoCurrencyHandler().parseAmount(cryptoAmount.sanitized());
} catch (e) {
amount = 0;
return Money.zero(cryptoCurrencyHandler());
}
return amount;
}
@observable
String estimatedFee;
Money estimatedFee;
@action
Future<void> calculateEstimatedFee() async {
@@ -169,7 +136,7 @@ abstract class OutputBase with Store {
if (_settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId) != null) {
fee = _wallet.calculateEstimatedFee(
_settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId)!,
formattedCryptoAmount,
cryptoAmountMoney.amount.toInt(),
);
}
@@ -179,22 +146,20 @@ abstract class OutputBase with Store {
case WalletType.litecoin:
case WalletType.bitcoinCash:
case WalletType.dogecoin:
estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee));
estimatedFee = Money.fromInt(fee, walletTypeToCryptoCurrency(_wallet.type));
break;
case WalletType.bitcoin:
if (cryptoCurrencyHandler() == CryptoCurrency.btcln) {
estimatedFee =
_appStore.amountParsingProxy.getDisplayCryptoString(10, cryptoCurrencyHandler());
estimatedFee = Money.fromInt(10, cryptoCurrencyHandler());
break;
}
if (_settingsStore.getPriority(_wallet.type) ==
bitcoin!.getBitcoinTransactionPriorityCustom()) {
fee = bitcoin!.getEstimatedFeeWithFeeRate(
_wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount);
_wallet, _settingsStore.customBitcoinFeeRate, cryptoAmountMoney.amount.toInt());
}
estimatedFee =
_appStore.amountParsingProxy.getDisplayCryptoString(fee, cryptoCurrencyHandler());
estimatedFee = Money.fromInt(fee, cryptoCurrencyHandler());
break;
/// EVMs
@@ -203,16 +168,20 @@ abstract class OutputBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
final isNative = cryptoCurrencyHandler() == CryptoCurrency.eth ||
cryptoCurrencyHandler() == CryptoCurrency.maticpoly ||
cryptoCurrencyHandler() == CryptoCurrency.baseEth ||
cryptoCurrencyHandler() == CryptoCurrency.arbEth ||
cryptoCurrencyHandler() == CryptoCurrency.bnb;
String? fee = isNative
final isNative = [
CryptoCurrency.eth,
CryptoCurrency.maticpoly,
CryptoCurrency.baseEth,
CryptoCurrency.arbEth,
CryptoCurrency.bnb,
].contains(cryptoCurrencyHandler());
final fee = isNative
? evm!.getEVMNativeEstimatedFee(_wallet)
: evm!.getEVMERC20EstimatedFee(_wallet);
estimatedFee = formatFixed(BigInt.parse(fee ?? '0.0'), 18, fractionalDigits: 12);
estimatedFee =
Money(BigInt.parse(fee ?? '0.0'), walletTypeToCryptoCurrency(_wallet.type));
break;
/// end EVMs
@@ -235,10 +204,11 @@ abstract class OutputBase with Store {
try {
final currency =
isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler();
isEVMCompatibleChain(_wallet.type) || cryptoCurrencyHandler() == CryptoCurrency.btcln
? _wallet.currency
: cryptoCurrencyHandler();
final cryptoAmount = double.parse(
_appStore.amountParsingProxy.getCanonicalCryptoAmount(estimatedFee, currency));
final cryptoAmount = double.parse(estimatedFee.toString());
return calculateFiatAmountRaw(
price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount);
@@ -252,7 +222,7 @@ abstract class OutputBase with Store {
WalletType get walletType => _wallet.type;
final CryptoCurrency Function() cryptoCurrencyHandler;
final CryptoCurrency Function([CryptoCurrency?]) cryptoCurrencyHandler;
final FiatConversionStore _fiatConversationStore;
final AppStore _appStore;
@@ -270,6 +240,7 @@ abstract class OutputBase with Store {
void updateWallet(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> newWallet) {
_wallet = newWallet;
estimatedFee = Money.zero(cryptoCurrencyHandler());
}
@action
@@ -283,10 +254,22 @@ abstract class OutputBase with Store {
resetParsedAddress();
}
@action
void resetParsedAddress() {
displayName = null;
extractedAddress = '';
parsedAddress = ParsedAddress(addresses: []);
note = '';
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {});
}
@action
void applyAddressLookupResult(ParsedAddress result) {
final currency = cryptoCurrencyHandler();
parsedAddress = result;
extractedAddress = result.parsedAddressByCurrencyMap[currency] ?? '';
note = result.description;
displayName = result.profileName.isNotEmpty ? result.profileName : result.handle;
}
@action
@@ -332,10 +315,21 @@ abstract class OutputBase with Store {
? CryptoCurrency.btc
: cryptoCurrencyHandler();
final price = _fiatConversationStore.prices[cryptoCurrency];
if (price == null || !price.isFinite || price <= 0) {
printV('_updateCryptoAmount: invalid fiat rate $price for $cryptoCurrency');
cryptoAmount = '';
return;
}
final decimals = min(20, cryptoCurrencyHandler().decimals);
final crypto = (double.parse(fiatAmount.replaceAll(',', '.')) /
_fiatConversationStore.prices[cryptoCurrency]!)
.toStringAsFixed(decimals);
final rawCrypto = double.parse(fiatAmount.replaceAll(',', '.')) / price;
if (!rawCrypto.isFinite) {
cryptoAmount = '';
return;
}
final crypto = rawCrypto.toStringAsFixed(decimals);
if (cryptoAmount != crypto) cryptoAmount = crypto;
} catch (e) {
@@ -346,29 +340,26 @@ abstract class OutputBase with Store {
Map<String, dynamic> get extra {
final fields = <String, dynamic>{};
if (parsedAddress.parseFrom == ParseFrom.bip353) {
fields['bip353_name'] = parsedAddress.name;
if (parsedAddress.addressSource == AddressSource.bip353) {
fields['bip353_name'] = parsedAddress.handle;
fields['bip353_proof'] = parsedAddress.bip353DnsProof;
}
return fields;
}
Future<void> fetchParsedAddress(BuildContext context) async {
final domain = address;
final currency = cryptoCurrencyHandler();
parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
if (parsedAddress.name.isNotEmpty) {
displayName = parsedAddress.name;
}
extractedAddress = await extractAddressFromParsed(context, parsedAddress);
note = parsedAddress.description;
}
@action
void loadContact(ContactBase contact) {
address = contact.name;
parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name);
extractedAddress = parsedAddress.addresses.first;
note = parsedAddress.description;
final currency = cryptoCurrencyHandler();
address = contact.address;
applyAddressLookupResult(
ParsedAddress(
parsedAddressByCurrencyMap: {currency: contact.address},
addressSource: AddressSource.contact,
handle: contact.name,
profileName: contact.name,
),
);
}
}
@@ -1,104 +0,0 @@
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/view_model/send/template_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:hash_wallet/entities/template.dart';
import 'package:hash_wallet/store/templates/send_template_store.dart';
import 'package:hash_wallet/core/template_validator.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_wallet/core/amount_validator.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
part 'send_template_view_model.g.dart';
class SendTemplateViewModel = SendTemplateViewModelBase with _$SendTemplateViewModel;
abstract class SendTemplateViewModelBase with Store {
final WalletBase _wallet;
final AppStore _appStore;
final SendTemplateStore _sendTemplateStore;
final FiatConversionStore _fiatConversationStore;
SendTemplateViewModelBase(
this._wallet, this._appStore, this._sendTemplateStore, this._fiatConversationStore)
: recipients = ObservableList<TemplateViewModel>() {
addRecipient();
}
ObservableList<TemplateViewModel> recipients;
@action
void addRecipient() {
recipients.add(TemplateViewModel(
wallet: _wallet, appStore: _appStore, fiatConversationStore: _fiatConversationStore));
}
@action
void removeRecipient(TemplateViewModel recipient) {
recipients.remove(recipient);
}
AmountValidator get amountValidator => AmountValidator(
currency: walletTypeToCryptoCurrency(_wallet.type, chainId: _wallet.chainId),
amountParsingProxy: _appStore.amountParsingProxy,
);
AddressValidator get addressValidator =>
AddressValidator(type: _wallet.currency, isTestnet: _wallet.isTestnet);
TemplateValidator get templateValidator => TemplateValidator();
bool get hasMultiRecipient => !isEVMCompatibleChain(_wallet.type);
@computed
CryptoCurrency get cryptoCurrency => _wallet.currency;
@computed
String get fiatCurrency => _appStore.settingsStore.fiatCurrency.title;
@computed
int get fiatCurrencyDecimals => _appStore.settingsStore.fiatCurrency.decimals;
@computed
ObservableList<Template> get templates => _sendTemplateStore.templates;
@action
void updateTemplate() => _sendTemplateStore.update();
@action
void addTemplate(
{required String name,
required bool isCurrencySelected,
required String cryptoCurrency,
required String address,
required String amount,
required String amountFiat,
required List<Template> additionalRecipients}) {
_sendTemplateStore.addTemplate(
name: name,
isCurrencySelected: isCurrencySelected,
address: address,
cryptoCurrency: cryptoCurrency,
fiatCurrency: fiatCurrency,
amount: amount,
amountFiat: amountFiat,
additionalRecipients: additionalRecipients);
updateTemplate();
}
@action
void removeTemplate({required Template template}) {
_sendTemplateStore.remove(template: template);
updateTemplate();
}
@computed
List<CryptoCurrency> get walletCurrencies => _wallet.balance.keys.toList();
bool get hasMultipleTokens => isEVMCompatibleChain(_wallet.type);
}
+192 -57
View File
@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_wallet/core/amount_parsing_proxy.dart';
import 'package:hash_wallet/core/amount_validator.dart';
@@ -8,6 +10,7 @@ import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/core/open_crypto_pay/exceptions.dart';
import 'package:hash_wallet/core/open_crypto_pay/models.dart';
import 'package:hash_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
import 'package:hash_wallet/core/utilities.dart';
import 'package:hash_wallet/core/validator.dart';
import 'package:hash_wallet/core/wallet_change_listener_view_model.dart';
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
@@ -15,7 +18,6 @@ import 'package:hash_wallet/entities/contact.dart';
import 'package:hash_wallet/entities/contact_record.dart';
import 'package:hash_wallet/entities/evm_transaction_error_fees_handler.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/entities/parsed_address.dart';
import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/entities/template.dart';
import 'package:hash_wallet/entities/transaction_description.dart';
@@ -38,16 +40,18 @@ import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/view_model/send/fees_view_model.dart';
import 'package:hash_wallet/view_model/send/output.dart';
import 'package:hash_wallet/view_model/send/send_template_view_model.dart';
import 'package:hash_wallet/view_model/send/send_view_model_state.dart';
import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/lnurl.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -85,8 +89,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
SendViewModelBase(
this._appStore,
this.sendTemplateViewModel,
this._fiatConversationStore,
this._adrResService,
this.balanceViewModel,
this.contactListViewModel,
this.transactionDescriptionBox,
@@ -105,11 +109,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
fiatFromSettings = _appStore.settingsStore.fiatCurrency,
fiatCurrencies = FiatCurrency.all,
super(appStore: _appStore) {
outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency));
outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler));
unspentCoinsListViewModel
.initialSetup()
.then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections());
unspentCoinsListViewModel.initialSetup();
// .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections());
reaction((_) {
if (isEVMCompatibleChain(wallet.type)) {
@@ -119,9 +122,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return null;
}, (_) async {
// When chain changes, update currencies and selected currency
final selectionAtChainChange = selectedCryptoCurrency;
await Future.delayed(const Duration(milliseconds: 100));
currencies = wallet.balance.keys.toList();
selectedCryptoCurrency = wallet.currency;
if (selectedCryptoCurrency == selectionAtChainChange) {
selectedCryptoCurrency = wallet.currency;
}
updateSendingBalance();
});
}
@@ -142,18 +148,48 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool get isEVMWallet => isEVMCompatibleChain(walletType);
CryptoCurrency _outputCryptoCurrencyHandler([CryptoCurrency? override]) {
if (override != null && override != selectedCryptoCurrency) {
runInAction(() => selectedCryptoCurrency = override);
}
return selectedCryptoCurrency;
}
@action
void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value;
@action
void addOutput() =>
outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency));
outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler));
@action
void removeOutput(Output output) {
if (isBatchSending) outputs.remove(output);
}
Future<ParsedAddress?> resolveAddressForOutput(Output output) async {
final query = output.address.trim();
if (query.isEmpty) return null;
// Check if the address is valid for the current currency (except for Zano, which can use handles as addresses)
if (selectedCryptoCurrency != CryptoCurrency.zano) {
final isValidAddress = AddressValidator(
type: selectedCryptoCurrency,
network: wallet.walletInfo.network,
).isValid(query);
if (isValidAddress) return null;
}
final results = await _adrResService.resolve(
query: query,
wallet: wallet,
currency: selectedCryptoCurrency,
);
return results.isEmpty ? null : results.first;
}
@action
void clearOutputs() {
outputs.clear();
@@ -264,8 +300,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Validator<String> get allAmountValidator => AllAmountValidator();
Validator<String> get addressValidator =>
AddressValidator(type: selectedCryptoCurrency, isTestnet: wallet.isTestnet);
Validator<String> get addressValidator => AddressValidator(
type: selectedCryptoCurrency,
isTestnet: wallet.isTestnet,
network: wallet.walletInfo.network,
);
Validator<String> get textValidator => TextValidator();
@@ -274,6 +313,24 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@observable
PendingTransaction? pendingTransaction;
// Bumped when a preparation is dismissed or replaced.
//
// Finished call sets pendingTransaction only if it matches this value to
// its own value.
int _sendPreparationId = 0;
String? get pendingTransactionAdditionalCostNotice {
final additionalCost = pendingTransaction?.additionalCost;
if (additionalCost == null) {
return null;
}
return S.current.recipient_account_creation_fee(
_appStore.amountParsingProxy.asDisplayStringWithSymbol(additionalCost),
);
}
@computed
String get balance {
if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.mweb) {
@@ -286,10 +343,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// Handle case where balance might not be available yet (e.g., during chain switch)
final balanceForCurrency = wallet.balance[selectedCryptoCurrency];
if (balanceForCurrency == null) {
return _appStore.amountParsingProxy.getDisplayCryptoString(0, selectedCryptoCurrency);
return _appStore.amountParsingProxy.asDisplayString(Money.zero(selectedCryptoCurrency));
}
return _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(
wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance, selectedCryptoCurrency);
return _appStore.amountParsingProxy
.asDisplayString(wallet.balance[selectedCryptoCurrency]!.available);
}
@action
@@ -316,7 +373,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// only for electrum, monero, wownero wallets atm:
switch (wallet.type) {
case WalletType.bitcoin:
if (coinTypeToSpendFrom == UnspentCoinType.lightning) return balance;
if (selectedCryptoCurrency == CryptoCurrency.btcln) return balance;
return _appStore.amountParsingProxy.getDisplayCryptoString(
await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom),
walletTypeToCryptoCurrency(walletType));
@@ -350,31 +407,20 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// If silent payments scanning, can still send payments
(wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus);
bool isSendToSilentPayments(Output output) =>
wallet.type == WalletType.bitcoin &&
(RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(output.address) ||
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
.hasMatch(output.extractedAddress) ||
(output.parsedAddress.addresses.isNotEmpty &&
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
.hasMatch(output.parsedAddress.addresses[0])));
bool isSendToSilentPayments(Output output) {
if (wallet.type != WalletType.bitcoin) return false;
final sp = RegExp(AddressValidator.silentPaymentAddressPatternMainnet);
final address = output.extractedAddress.isNotEmpty ? output.extractedAddress : output.address;
return sp.hasMatch(address);
}
@computed
List<Template> get templates => sendTemplateViewModel.templates
.where((template) => _isEqualCurrency(template.cryptoCurrency))
.toList();
@computed
bool get hasCoinControl =>
[
WalletType.bitcoin,
WalletType.litecoin,
WalletType.monero,
WalletType.wownero,
WalletType.bitcoinCash,
WalletType.dogecoin
].contains(wallet.type) &&
coinTypeToSpendFrom != UnspentCoinType.lightning;
wallet.hasCoinControl && coinTypeToSpendFrom != UnspentCoinType.lightning;
@computed
bool get hasFees => feesViewModel.hasFees && coinTypeToSpendFrom != UnspentCoinType.lightning;
@@ -394,6 +440,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
String get selectedCryptoCurrencySymbol =>
amountParsingProxy.getCryptoSymbol(selectedCryptoCurrency);
@computed
bool get useBaseUnits => amountParsingProxy.useSatoshi(selectedCryptoCurrency);
List<CryptoCurrency> currencies;
WalletType get walletType => wallet.type;
@@ -411,20 +460,20 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final AppStore _appStore;
SettingsStore get _settingsStore => _appStore.settingsStore;
final SendTemplateViewModel sendTemplateViewModel;
final BalanceViewModel balanceViewModel;
final ContactListViewModel contactListViewModel;
final HardwareWalletViewModel? hardwareWalletViewModel;
final FeesViewModel feesViewModel;
final FiatConversionStore _fiatConversationStore;
final AddressResolverService _adrResService;
final Box<TransactionDescription> transactionDescriptionBox;
@computed
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
@computed
bool get hasMultiRecipient =>
sendTemplateViewModel.hasMultiRecipient && coinTypeToSpendFrom != UnspentCoinType.lightning;
!isEVMCompatibleChain(wallet.type) &&
coinTypeToSpendFrom != UnspentCoinType.lightning;
@computed
String get languageCode => _appStore.settingsStore.languageCode;
@@ -464,6 +513,19 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool get shouldDisplayTOTP2FAForSendsToInternalWallet =>
_settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
@computed
TransactionInfo? get transactionInfo {
if (isEVMCompatibleChain(walletType)) {
return wallet.transactionHistory.transactions[pendingTransaction?.evmTxHashFromRawHex];
}
if (walletType == WalletType.monero) {
// monero tx history keys are in format txhash_amount_accountindex_addressindex
return wallet.transactionHistory.transactions[wallet.transactionHistory.transactions.keys
.firstWhereOrNull((item) => item.split("_").first == pendingTransaction?.id)];
}
return wallet.transactionHistory.transactions[pendingTransaction?.id];
}
//* Still open to further optimize these checks
//* It works but can be made better
@action
@@ -498,6 +560,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@action
Future<void> dismissTransaction() async {
_sendPreparationId++;
_ledgerTxStateTimer?.cancel();
_ledgerTxStateTimer = null;
state = InitialExecutionState();
if (ocpRequest != null) {
clearOutputs();
@@ -550,8 +615,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
clearOutputs();
outputs.first.address = paymentRequest.address;
outputs.first.parsedAddress =
ParsedAddress(addresses: [paymentRequest.address], name: ocpRequest!.receiverName);
outputs.first.parsedAddress = ParsedAddress(
parsedAddressByCurrencyMap: {currency: paymentRequest.address},
handle: ocpRequest!.receiverName);
outputs.first.setCryptoAmount(paymentRequest.amount);
outputs.first.note = ocpRequest!.receiverName;
@@ -587,20 +653,38 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@action
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider, Trade? trade}) async {
final preparationId = ++_sendPreparationId;
_ledgerTxStateTimer?.cancel();
_ledgerTxStateTimer = null;
pendingTransaction = null;
Timer? preparationTimer;
bool replaced() {
if (preparationId == _sendPreparationId) {
return false;
}
preparationTimer?.cancel();
return true;
}
try {
if (!(state is IsExecutingState)) state = IsExecutingState();
if (wallet.isHardwareWallet) {
state = IsAwaitingDeviceResponseState();
if (walletType == WalletType.monero) {
_ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
preparationTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (preparationId != _sendPreparationId) {
timer.cancel();
return;
}
if (monero!.getLastLedgerCommand() == "INS_CLSAG") {
timer.cancel();
state = IsDeviceSigningResponseState();
}
});
_ledgerTxStateTimer = preparationTimer;
} else {
state = IsAwaitingDeviceResponseState();
}
}
@@ -609,19 +693,32 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final isSendAll = outputs.any((output) => output.sendAll);
if (!isSendAll) {
final estimateTxAmountDouble = outputs.fold<double>(
0, (acc, output) => acc + (double.tryParse(output.cryptoAmount) ?? 0));
if (estimateTxAmountDouble <= 0) throw Exception('Amount must be greater than 0');
final estimateTxAmount = outputs.fold<BigInt>(
BigInt.zero, (acc, output) => acc + output.cryptoAmountMoney.amount);
if (estimateTxAmount <= BigInt.zero) {
throw Exception('Amount must be greater than 0');
}
}
pendingTransaction = await wallet.createTransaction(_credentials(provider));
final prepared = await wallet.createTransaction(_credentials(provider));
if (replaced()) {
return null;
}
pendingTransaction = prepared;
final txAmountDouble = double.tryParse(pendingTransaction?.amountFormatted ?? '0') ?? 0.0;
final bool isTradeTx = trade != null && provider != null;
if (isTradeTx) {
final tradeAmountDouble = double.tryParse(trade.amount) ?? 0.0;
if (tradeAmountDouble <= 0) throw Exception('Trade amount must be greater than 0');
final tradeAmountMoney = Money.tryParse(
trade.amount.sanitized(),
trade.from ?? selectedCryptoCurrency,
strictParsing: false,
);
if (tradeAmountMoney == null || tradeAmountMoney.sign <= 0) {
throw Exception('Trade amount must be greater than 0');
}
final tradeAmountDouble = double.tryParse(tradeAmountMoney.toString()) ?? 0.0;
if (trade.isSendAll == true) {
if (provider is NearIntentsExchangeProvider) {
@@ -644,6 +741,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
state = ExecutedSuccessfullyState();
return pendingTransaction;
} catch (e) {
if (replaced()) {
return null;
}
_ledgerTxStateTimer?.cancel();
// if (e is LedgerException) {
// final errorCode = e.errorCode.toRadixString(16);
@@ -721,7 +821,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
try {
state = TransactionCommitting();
state = wallet.isHardwareWallet && walletType == WalletType.monero
? IsAwaitingDeviceResponseState()
: TransactionCommitting();
if (ocpRequest != null) {
await _handleOcpRequest();
@@ -747,6 +849,28 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
});
}
// FIXME(malik) ideally, this should be done wallet-side.
// it is required because evm, solana and tron don't actually save the transaction info when you send something.
// instead, they rely on the tx to eventually get fetched at sync time, which can take a while
if (isEVMWallet) {
final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull(
(token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(),
);
wallet.transactionHistory.addOne(evm!.getTransactionInfo(
id: pendingTransaction!.evmTxHashFromRawHex!,
height: 0,
amount: outputs.first.cryptoAmountMoney,
fee: pendingTransaction!.fee,
tokenSymbol: selectedCryptoCurrency.title,
direction: TransactionDirection.outgoing,
isPending: true,
date: DateTime.now(),
confirmations: 0,
chainId: wallet.chainId ?? 0,
contractAddress: selectedToken?.contractAddress,
));
}
if (pendingTransaction!.id.isNotEmpty) {
_addTransactionDescription();
}
@@ -763,9 +887,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Future<void> _addTransactionDescription() async {
String address = outputs.fold('', (acc, value) {
return value.isParsedAddress
? '$acc${value.address}\n${value.extractedAddress}\n\n'
: '$acc${value.address}\n\n';
final canonical = value.extractedAddress.trim().isNotEmpty
? value.extractedAddress.trim()
: value.address.trim();
if (canonical.isEmpty) {
return acc;
}
return '$acc$canonical\n\n';
});
address = address.trim();
@@ -891,8 +1019,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
for (final output in outputs) {
final address =
output.isParsedAddress ? output.parsedAddress.addresses.first : output.address;
final address = output.isParsedAddress
? output.parsedAddress.parsedAddressByCurrencyMap[selectedCryptoCurrency] ?? ''
: output.address;
if (address.isNotEmpty &&
!contactAddresses.contains(address) &&
@@ -1035,9 +1164,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
String? payjoinUri;
@action
Future<void> fetchTokenForContractAddress(String contractAddress) async {
Future<void> fetchTokenForContractAddress(String contractAddress,
{WalletType? walletType}) async {
final token = await TokenUtilities.findTokenByAddress(
walletType: wallet.type,
walletType: walletType ?? wallet.type,
address: contractAddress,
);
@@ -1045,4 +1175,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
selectedCryptoCurrency = token;
}
}
@action
void applyAnyPayCurrency(CryptoCurrency currency) {
selectedCryptoCurrency = currency;
}
}
@@ -1,75 +0,0 @@
import 'package:hash_wallet/entities/template.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/view_model/send/output.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart';
part 'template_view_model.g.dart';
class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel;
abstract class TemplateViewModelBase with Store {
final WalletBase _wallet;
final AppStore _appStore;
final FiatConversionStore _fiatConversationStore;
TemplateViewModelBase({
required WalletBase wallet,
required AppStore appStore,
required FiatConversionStore fiatConversationStore,
}) : _wallet = wallet,
_appStore = appStore,
_fiatConversationStore = fiatConversationStore,
_currency = wallet.currency,
output = Output(wallet, appStore, fiatConversationStore, () => wallet.currency) {
output = Output(_wallet, _appStore, _fiatConversationStore, () => _currency);
}
@observable
Output output;
@observable
String name = '';
@observable
String address = '';
@observable
CryptoCurrency _currency;
@observable
bool isCryptoSelected = true;
@action
void setCryptoCurrency(bool value) => isCryptoSelected = value;
@action
void reset() {
name = '';
address = '';
isCryptoSelected = true;
output.reset();
}
Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) {
return Template(
isCurrencySelectedRaw: isCryptoSelected,
nameRaw: name,
addressRaw: address,
cryptoCurrencyRaw: cryptoCurrency,
fiatCurrencyRaw: fiatCurrency,
amountRaw: output.cryptoAmount,
amountFiatRaw: output.fiatAmount);
}
@action
void changeSelectedCurrency(CryptoCurrency currency) {
isCryptoSelected = true;
_currency = currency;
}
@computed
CryptoCurrency get selectedCurrency => _currency;
}